Tutorial on how to upgrade a proxied ERC20 contract for use with Superchain interop.
The SuperchainERC20 standard is ready for production deployments.
Please note that the OP Stack interoperability upgrade, required for cross-chain messaging, is currently still in active development.
This guide explains how to upgrade an ERC20 to a SuperchainERC20 that can teleport across the Superchain interop cluster when the original ERC20 contract was placed behind a proxy to enable future upgrades.
Show About this tutorial
What you’ll learn
How to upgrade an ERC20 token to enable Superchain interoperability when it was deployed with a proxy.
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.
A beacon proxy uses two contracts.
The UpgradeableBeacon contract holds the address of the implementation contract.
The BeaconProxy 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 to call that contract.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.
Verify that the proxy address is the same as $ERC20_ADDRESS, and that the beacon address is the same as $BEACON_ADDRESS.
Show What to do when the values are not the same
This can happen when the nonce values of 0xf39Fd6e51aad88F6F4ce6aB8827279cffFb92266 (or your address in the case of using devnet) on chain A and chain B are different.You can see the nonce values using these commands:
If you do this, remember to update $ERC20_ADDRESS and $BEACON_ADDRESS.If the nonce on chain B is already higher than the nonce was on chain A when the original proxy contract was deployed this method is not available and you have to either create a special bridge or use a lockbox.
7
Deploy ERC7802 contracts
We need to replace the ERC20 contracts with contracts that:
Have the same storage layout as the ERC20 contracts they replace.
These contracts do not need to be deployed to the same address.
The address that needs to be the same is not the address of the ERC20 contract itself, but of the proxy.
Create a file, src/InteropToken.sol:
Report incorrect code
Copy
Ask AI
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; } }
Most of the code is identical to the original MyToken.
Report incorrect code
Copy
Ask AI
import {IERC7802, IERC165} from "lib/interop-lib/src/interfaces/IERC7802.sol";import {PredeployAddresses} from "lib/interop-lib/src/libraries/PredeployAddresses.sol";
These are the imports needed for ERC7802 support.
We need IERC165 for documentation purposes, and IERC7802 for the ERC7802 events.
Report incorrect code
Copy
Ask AI
contract InteropToken is Initializable, ERC20Upgradeable, OwnableUpgradeable, IERC7802 {
We also implement ERC165, but we don’t need to import anything from there.
Report incorrect code
Copy
Ask AI
function initialize(string memory name, string memory symbol, uint256 initialSupply) public initializer { __ERC20_init(name, symbol); __Ownable_init(msg.sender); _mint(msg.sender, initialSupply); }
This function is identical to the one in MyToken.
Report incorrect code
Copy
Ask AI
/// @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);}
Copying the original ERC20 token code with minimal differences is one method to keep the storage layout identical.
Alternatively, if you want to use a different contract, such as SuperchainERC20, you can modify the storage layout to match the old one using the Solidity docs.
Deploy this contract on both chains, and store the addresses (which may or may not be the same).