SuperchainERC20
is an implementation of ERC-7802 designed to enable asset interoperability in the Superchain.
SuperchainERC20
tokens are fungible across the Superchain by giving the SuperchainERC20Bridge
permission to mint and burn the token during cross-chain transfers. For more information on SuperchainERC20 please visit the docs.
Note: ERC20 tokens that do not utilize the SuperchainERC20Bridge
for cross-chain transfers can still achieve fungibility across the Superchain through interop message passing with a custom bridge solution. For these custom tokens, implementing ERC-7802 is strongly recommended, as it unifies cross-chain mint and burn interfaces, enabling tokens to benefit from a standardized approach to cross-chain transfers.
To achieve cross-chain functionality, the SuperchainERC20
standard incorporates the IERC7802
interface, defining essential functions and events:
crosschainMint
: Mints tokens on the destination chain as part of a cross-chain transfer.crosschainBurn
: Burns tokens on the source chain to facilitate the transfer.- Events (
CrosschainMint
andCrosschainBurn
): Emit when tokens are minted or burned, enabling transparent tracking of cross-chain transactions.
supersim
requires anvil
to be installed.
Follow this guide to install Foundry.
git clone [email protected]:ethereum-optimism/superchainerc20-starter.git
cd superchainerc20-starter
pnpm i
pnpm init:env
This command will:
- Start the
supersim
local development environment - Deploy the smart contracts to the test networks
- Launch the example frontend application
pnpm dev
This repository includes a script to automatically fetch the public RPC URLs for each chain listed in the Superchain Registry and add them to the [rpc_endpoints]
configuration section of foundry.toml
.
The script ensures that only new RPC URLs are appended, preserving any URLs already present in foundry.toml
. To execute this script, run:
pnpm contracts:update:rpcs
The deployment configuration for token deployments is managed through the deploy-config.toml
file. Below is a detailed breakdown of each configuration section:
This section defines parameters for deploying token contracts.
salt
: A unique identifier used for deploying token contracts via [Create2
]. This value along with the contract bytecode ensures that contract deployments are deterministic.- example:
salt = "ethers phoenix"
- example:
chains
: Lists the chains where the token will be deployed. Each chain must correspond to an entry in the[rpc_endpoints]
section offoundry.toml
.- example:
chains = ["op_chain_a","op_chain_b"]
- example:
Deployment configuration for the token that will be deployed.
owner_address
: the address designated as the owner of the token.- The
L2NativeSuperchainERC20.sol
contract included in this repo extends theOwnable
contract - example:
owner_address = "0xf39Fd6e51aad88F6F4ce6aB8827279cffFb92266"
- The
name
: the token's name.- example:
name = "TestSuperchainERC20"
- example:
symbol
: the token's symbol.- example:
symbol = "TSU"
- example:
decimals
: the number of decimal places the token supports.- example:
decimals = 18
- example:
Before proceeding with this section, ensure that your deploy-config.toml
file is fully configured (see the Deployment config section for more details on setup). Additionally, confirm that the [rpc_endpoints]
section in foundry.toml
is properly set up by following the instructions in Configuring RPC urls.
Deployments are executed through the SuperchainERC20Deployer.s.sol
script. This script deploys tokens across each specified chain in the deployment configuration using Create2
, ensuring deterministic contract addresses for each deployment. The script targets the L2NativeSuperchainERC20.sol
contract by default. If you need to modify the token being deployed, either update this file directly or point the script to a custom token contract of your choice.
To execute a token deployment run:
pnpm contracts:deploy:token
Create2
ensures that the address is deterministically determined by the bytecode of the contract and the provided salt. This is crucial because in order for cross-chain transfers of SuperchainERC20
s to work with interop, the tokens must be deployed at the same address across all chains.
For best security practices SuperchainERC20Bridge
should be the only contract with permission to call crosschainMint
and crosschainBurn
. These permissions are set up by default when using the SuperchainERC20
contract.
The packages/e2e-test
directory contains simple end-to-end integration tests using vitest
that run against supersim
Before running the tests, ensure you have:
- Completed all steps in the Getting Started section
- Initialized your environment variables (
pnpm init:env
)
pnpm e2e-test
The tests will run against your local supersim instance.
Note: Interop is currently in active development and not yet ready for production use. This example uses supersim in order to demonstrate how cross-chain transfers will work once interop is live.
Note: this example uses a pre-funded test account provided by anvil for all transactions and as the owner of the L2NativeSuperchainERC20
. The address of this account is 0xf39Fd6e51aad88F6F4ce6aB8827279cffFb92266
and private key is 0xac0974bec39a17e36ba4a6b4d238ff944bacb478cbed5efcae784d7bf4f2ff80
1. Follow the steps in the Getting started section.
After completing these steps, you will have the following set up:
supersim
running in autorelay mode with two L2 chains- The
L2NativeSuperchainERC20
token deployed on both chains
2. Find the address and owner of the L2NativeSuperchainERC20 token that was deployed.
The address that the token in step 1 was deployed to and the address of the owner of the token can be found in the deployment.json file under the "deployedAddress"
and "ownerAddress"
fields. The deployedAddress
address will be used for any token interactions in the next steps and the private key of the ownerAddress
will need to be used for step 3 since minting requires owner privileges. In this example deployment.json
file the token in step 1 was deployed at 0x5BCf71Ca0CE963373d917031aAFDd6D98B80B159
and the owner address is 0xf39Fd6e51aad88F6F4ce6aB8827279cffFb92266
:
# example
{
"deployedAddress": "0x5BCf71Ca0CE963373d917031aAFDd6D98B80B159",
"ownerAddress": "0xf39Fd6e51aad88F6F4ce6aB8827279cffFb92266"
}
3. Mint tokens to transfer on chain 901
The following command creates a transaction using cast
to mint 1000 L2NativeSuperchainERC20
tokens. Replace <deployed-token-address>
with the deployedAddress
from deployment.json, replace <recipient-address>
with the address you would like to send the tokens to, and replace <owner-address-private-key>
with the private key of the ownerAddress
from step deployment.json.
cast send <deployed-token-address> "mintTo(address _to, uint256 _amount)" <recipient-address> 1000 --rpc-url http://127.0.0.1:9545 --private-key <owner-address-private-key>
4. Initiate the send transaction on chain 901
Send the tokens from Chain 901 to Chain 902 by calling SendERC20
on the SuperchainTokenBridge
. The SuperchainTokenBridge
is an OP Stack predeploy and can be located at address 0x4200000000000000000000000000000000000028
. Replace <deployed-token-address>
with the deployedAddress
from deployment.json, replace <recipient-address>
with the address you would like to send the tokens to, and replace <token-sender-private-key>
with the private key of the recipient address from step 3.
cast send 0x4200000000000000000000000000000000000028 "sendERC20(address _token, address _to, uint256 _amount, uint256 _chainId)" <deployed-token-address> <recipient-address> 1000 902 --rpc-url http://127.0.0.1:9545 --private-key <token-sender-private-key>
5. Wait for the relayed message to appear on chain 902
In a few seconds, you should see the RelayedMessage on chain 902:
# example
INFO [11-01|16:02:25.089] SuperchainTokenBridge#RelayERC20 token=0x5BCf71Ca0CE963373d917031aAFDd6D98B80B159 from=0xf39Fd6e51aad88F6F4ce6aB8827279cffFb92266 to=0x70997970C51812dc3A010C7d01b50e0d17dc79C8 amount=1000 source=901
6. Check the balance on chain 902
Verify that the balance for the recipient of the L2NativeSuperchainERC20
on chain 902 has increased:
cast balance --erc20 <deployed-token-address> <recipient-address> --rpc-url http://127.0.0.1:9546
This section explains how to modify an existing ERC20
contract to make it interoperable within the Superchain.
In this example, we will update the GovernanceToken ERC20
to make it interoperable. To do this we just need to implement the IERC7802 interface.
Note: The IERC7802
interface is designed to be minimal and only specifies the required function signatures (crosschainMint
and crosschainBurn
) and events (CrosschainMint
and CrosschainBurn
). Developers have complete control over how these functions are implemented, including the logic for handling cross-chain minting and burning. This allows flexibility to tailor the behavior to specific use cases or custom bridge solutions. In the example below we show how this interface is implemented for an example token that uses the SuperchainTokenBridge
for cross-chain minting and burning.
1. Add the IERC7802 interface to the contract
Import the IERC7802
and update your contract to inherit from it.
import { IERC7802 } from "@contracts-bedrock/L2/interfaces/IERC7802.sol";
contract GovernanceToken is IERC7802, ERC20Burnable, ERC20Votes, Ownable {
// Contract implementation here
}
2. Add the crosschainMint
function to the contract
The crosschainMint
function defines how tokens are minted on the destination chain during a cross-chain transfer. While the interface requires the function's signature, the implementation is entirely up to you.
In this example, only the SuperchainTokenBridge
contract will have permissions to mint the token during a crosschain transfer. If you’re using a custom bridge, you could replace SuperchainTokenBridge
with your bridge contract and implement the desired logic. You can customize this function to add any logic or access control as needed.
/// @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 {
// Only the `SuperchainTokenBridge` has permissions to mint tokens during crosschain transfers.
if (msg.sender != Predeploys.SUPERCHAIN_TOKEN_BRIDGE) revert Unauthorized();
// Mint tokens to the `_to` account's balance.
_mint(_to, _amount);
// Emit the CrosschainMint event included on IERC7802 for tracking token mints associated with cross chain transfers.
emit CrosschainMint(_to, _amount, msg.sender);
}
3. Add the crosschainBurn
function to the contract
The crosschainBurn
function defines how tokens are burned on the source chain during a cross-chain transfer. Again, while the interface specifies the function signature, the implementation is completely flexible.
In this example, only the SuperchainTokenBridge
contract will have permissions to burn the token during a crosschain transfer. However, you can modify this to fit your requirements.
/// @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 {
// Only the `SuperchainTokenBridge` has permissions to burn tokens during crosschain transfers.
if (msg.sender != Predeploys.SUPERCHAIN_TOKEN_BRIDGE) revert Unauthorized();
// Burn the tokens from the `_from` account's balance.
_burn(_from, _amount);
// Emit the CrosschainBurn event included on IERC7802 for tracking token burns associated with cross chain transfers.
emit CrosschainBurn(_from, _amount, msg.sender);
}
4. Add the supportsInterface
function to the contract
Since IERC7802
is an extension of IERC165, your contract must implement the supportsInterface
function to indicate which interfaces it supports.
In this example our token supports IERC7802
, IERC20
, and IERC165
/// @notice Query if a contract implements an interface
/// @param interfaceID The interface identifier, as specified in ERC-165
/// @dev Interface identification is specified in ERC-165. This function
/// uses less than 30,000 gas.
/// @return `true` if the contract implements `interfaceID` and
/// `interfaceID` is not 0xffffffff, `false` otherwise
function supportsInterface(bytes4 _interfaceId) public view virtual returns (bool) {
return _interfaceId == type(IERC7802).interfaceId || _interfaceId == type(IERC20).interfaceId
|| _interfaceId == type(IERC165).interfaceId;
}
After following these four steps your token is ready to be interoperable! The full example contract is available here.
This example showcases that the IERC7802
interface does not enforce any specific logic for handling cross-chain minting and burning. It only ensures that the required functions and events exist, leaving all implementation details entirely up to you. You can:
- Use any custom access control mechanisms for
crosschainMint
andcrosschainBurn
. - Implement specific checks, restrictions, or business logic tailored to your token's requirements.
- Integrate the interface with your own bridge or use it with the
SuperchainTokenBridge
.
Contributions are encouraged, but please open an issue before making any major changes to ensure your changes will be accepted.
Files are licensed under the MIT license.