Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -18,23 +18,22 @@ is_imported_content: 'false'
import { Callout, Steps } from 'nextra/components'
import { WipCallout } from '@/components/WipCallout'

<WipCallout />
# Bridging your custom ERC-20 token using the Standard Bridge

In this tutorial you'll learn how to bridge a custom ERC-20 token from Ethereum to an OP Stack chain using the Standard Bridge system.
In this tutorial, you'll learn how to bridge a custom ERC-20 token from Ethereum to an OP Stack chain using the Standard Bridge system.
This tutorial is meant for developers who already have an existing ERC-20 token on Ethereum and want to create a bridged representation of that token on OP Mainnet.

This tutorial explains how you can create a custom token that conforms to the [`IOptimismMintableERC20`](https://github.com/ethereum-optimism/optimism/blob/v1.1.4/packages/contracts-bedrock/src/universal/IOptimismMintableERC20.sol) interface so that it can be used with the Standard Bridge system.
This tutorial explains how you can create a custom token that conforms to the [`IOptimismMintableERC20`](https://github.com/ethereum-optimism/optimism/blob/v1.12.2/packages/contracts-bedrock/src/universal/IOptimismMintableERC20.sol) interface so that it can be used with the Standard Bridge system.
A custom token allows you to do things like trigger extra logic whenever a token is deposited.
If you don't need extra functionality like this, consider following the tutorial on [Bridging Your Standard ERC-20 Token Using the Standard Bridge](./standard-bridge-standard-token) instead.

<Callout type="error">
The Standard Bridge **does not** support [**fee on transfer tokens**](https://github.com/d-xo/weird-erc20#fee-on-transfer) or [**rebasing tokens**](https://github.com/d-xo/weird-erc20#balance-modifications-outside-of-transfers-rebasingairdrops) because they can cause bridge accounting errors.
The Standard Bridge **does not** support [**fee on transfer tokens**](https://github.com/d-xo/weird-erc20#fee-on-transfer) or [**rebasing tokens**](https://github.com/d-xo/weird-erc20#balance-modifications-outside-of-transfers-rebasingairdrops) because they can cause bridge accounting errors.
</Callout>

## About OptimismMintableERC20s

The Standard Bridge system requires that L2 representations of L1 tokens implement the [`IOptimismMintableERC20`](https://github.com/ethereum-optimism/optimism/blob/v1.1.4/packages/contracts-bedrock/src/universal/IOptimismMintableERC20.sol) interface.
The Standard Bridge system requires that L2 representations of L1 tokens implement the [`IOptimismMintableERC20`](https://github.com/ethereum-optimism/optimism/blob/v1.12.2/packages/contracts-bedrock/src/universal/IOptimismMintableERC20.sol) interface.
This interface is a superset of the standard ERC-20 interface and includes functions that allow the bridge to properly verify deposits/withdrawals and mint/burn tokens as needed.
Your L2 token contract must implement this interface in order to be bridged using the Standard Bridge system.
This tutorial will show you how to create a custom token that implements this interface.
Expand All @@ -50,8 +49,8 @@ This tutorial explains how to create a bridged ERC-20 token on OP Sepolia.
You will need to get some ETH on both of these testnets.

<Callout type="info">
You can use [this faucet](https://sepoliafaucet.com/) to get ETH on Sepolia.
You can use the [Superchain Faucet](https://console.optimism.io/faucet?utm_source=docs) to get ETH on OP Sepolia.
You can use [this faucet](https://sepoliafaucet.com/) to get ETH on Sepolia.
You can use the [Superchain Faucet](https://console.optimism.io/faucet?utm_source=docs) to get ETH on OP Sepolia.
</Callout>

## Add OP Sepolia to your wallet
Expand All @@ -76,50 +75,54 @@ In this section, you'll be creating an ERC-20 token that can be deposited but ca
This is just one example of the endless ways in which you could customize your L2 token.

<Steps>
{<h3>Open Remix</h3>}

{<h3>Open Remix</h3>}
Navigate to [Remix](https://remix.ethereum.org) in your browser.

Navigate to [Remix](https://remix.ethereum.org) in your browser.
{<h3>Create a new file</h3>}

{<h3>Create a new file</h3>}
Click the 📄 ("Create new file") button to create a new empty Solidity file.
You can name this file whatever you'd like, for example `MyCustomL2Token.sol`.

Click the 📄 ("Create new file") button to create a new empty Solidity file.
You can name this file whatever you'd like.
{<h3>Copy the example contract</h3>}

{<h3>Copy the example contract</h3>}
Copy the following example contract into your new file:

Copy the following example contract into your new file:
```solidity file=<rootDir>/public/tutorials/standard-bridge-custom-token.sol#L1-L189 hash=07ca2fe7fcbbbe2dc06c07cb1fb72d91
```

```solidity file=<rootDir>/public/tutorials/standard-bridge-custom-token.sol#L1-L97 hash=a0b97f33ab7bff9ceb8271b8fa4fd726
```
{<h3>Review the example contract</h3>}

{<h3>Review the example contract</h3>}
Take a moment to review the example contract. It's closely based on the official [`OptimismMintableERC20`](https://github.com/ethereum-optimism/optimism/blob/v1.12.2/packages/contracts-bedrock/src/universal/OptimismMintableERC20.sol) contract with one key modification:

Take a moment to review the example contract.
It's almost the same as the standard [`OptimismMintableERC20`](https://github.com/ethereum-optimism/optimism/blob/v1.1.4/packages/contracts-bedrock/src/universal/OptimismMintableERC20.sol) contract except that the `_burn` function has been made to always revert.
Since the bridge needs to burn tokens when users want to withdraw them to L1, this means that users will not be able to withdraw tokens from this contract.
The `burn` function has been modified to always revert, making it impossible to withdraw tokens back to L1.

```solidity file=<rootDir>/public/tutorials/standard-bridge-custom-token.sol#L85-L96 hash=7c8cdadf1bec4c76dafb5552d1a593fe
```
Since the bridge needs to burn tokens when users want to withdraw them to L1, this means that users will not be able to withdraw tokens from this contract. Here's the key part of the contract that prevents withdrawals:

{<h3>Compile the contract</h3>}
```solidity file=<rootDir>/public/tutorials/standard-bridge-custom-token.sol#L136-L156 hash=632f4649d3ce66c28ec34f58046a8890
```

Save the file to automatically compile the contract.
If you've disabled auto-compile, you'll need to manually compile the contract by clicking the "Solidity Compiler" tab (this looks like the letter "S") and press the blue "Compile" button.
{<h3>Compile the contract</h3>}

{<h3>Deploy the contract</h3>}
Save the file to automatically compile the contract.
If you've disabled auto-compile, you'll need to manually compile the contract by clicking the "Solidity Compiler" tab (this looks like the letter "S") and press the blue "Compile" button.

Open the deployment tab (this looks like an Ethereum logo with an arrow pointing left).
Make sure that your environment is set to "Injected Provider", your wallet is connected to OP Sepolia, and Remix has access to your wallet.
Then, select the `MyCustomL2Token` contract from the deployment dropdown and deploy it with the following parameters:
Make sure you're using Solidity compiler version 0.8.15 (the same version used in the official Optimism contracts).

```text
_BRIDGE: "0x4200000000000000000000000000000000000010"
_REMOTETOKEN: "<L1 ERC-20 address>"
_NAME: "My Custom L2 Token"
_SYMBOL: "MCL2T"
```
{<h3>Deploy the contract</h3>}

Open the deployment tab (this looks like an Ethereum logo with an arrow pointing left).
Make sure that your environment is set to "Injected Provider", your wallet is connected to OP Sepolia, and Remix has access to your wallet.
Then, select the `MyCustomL2Token` contract from the deployment dropdown and deploy it with the following parameters:

```text
_bridge: "0x4200000000000000000000000000000000000010" // L2 Standard Bridge address
_remoteToken: "<L1 ERC-20 address>" // Your L1 token address
_name: "My Custom L2 Token" // Your token name
_symbol: "MCL2T" // Your token symbol
```

Note: The L2 Standard Bridge address is a predefined address on all OP Stack chains, so it will be the same on OP Sepolia and OP Mainnet.
</Steps>

## Bridge some tokens
Expand Down
159 changes: 126 additions & 33 deletions public/tutorials/standard-bridge-custom-token.sol
Original file line number Diff line number Diff line change
@@ -1,12 +1,84 @@
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.0;
pragma solidity 0.8.20;

// Import the standard ERC20 implementation from OpenZeppelin
import { ERC20 } from "@openzeppelin/contracts/token/ERC20/ERC20.sol";
import { IERC165 } from "@openzeppelin/contracts/utils/introspection/IERC165.sol";
import { IOptimismMintableERC20 } from "https://github.com/ethereum-optimism/optimism/blob/v1.1.4/packages/contracts-bedrock/src/universal/IOptimismMintableERC20.sol";

contract MyCustomL2Token is IOptimismMintableERC20, ERC20 {
/// @notice Address of the corresponding version of this token on the remote chain.
/**
* @title ILegacyMintableERC20
* @notice Legacy interface for the StandardL2ERC20 contract.
*/
interface ILegacyMintableERC20 {
function mint(address _to, uint256 _amount) external;
function burn(address _from, uint256 _amount) external;

function l1Token() external view returns (address);
function l2Bridge() external view returns (address);
}

/**
* @title IOptimismMintableERC20
* @notice Interface for the OptimismMintableERC20 contract.
*/
interface IOptimismMintableERC20 {
function remoteToken() external view returns (address);
function bridge() external view returns (address);
function mint(address _to, uint256 _amount) external;
function burn(address _from, uint256 _amount) external;
}

/**
* @title Simplified Semver for tutorial
* @notice Simple contract to track semantic versioning
*/
contract Semver {
string public version;

// Simple function to convert uint to string for version numbers
function toString(uint256 value) internal pure returns (string memory) {
// This function handles numbers from 0 to 999 which is sufficient for versioning
if (value == 0) {
return "0";
}

uint256 temp = value;
uint256 digits;

while (temp != 0) {
digits++;
temp /= 10;
}

bytes memory buffer = new bytes(digits);

while (value != 0) {
digits -= 1;
buffer[digits] = bytes1(uint8(48 + uint256(value % 10)));
value /= 10;
}

return string(buffer);
}

constructor(uint256 major, uint256 minor, uint256 patch) {
version = string(abi.encodePacked(
toString(major),
".",
toString(minor),
".",
toString(patch)
));
}
}

/**
* @title MyCustomL2Token
* @notice A custom L2 token based on OptimismMintableERC20 that can be deposited
* from L1 to L2, but cannot be withdrawn from L2 to L1.
*/
contract MyCustomL2Token is IOptimismMintableERC20, ILegacyMintableERC20, ERC20, Semver {
/// @notice Address of the corresponding token on the remote chain.
address public immutable REMOTE_TOKEN;

/// @notice Address of the StandardBridge on this network.
Expand All @@ -22,7 +94,7 @@ contract MyCustomL2Token is IOptimismMintableERC20, ERC20 {
/// @param amount Amount of tokens burned.
event Burn(address indexed account, uint256 amount);

/// @notice A modifier that only allows the bridge to call.
/// @notice A modifier that only allows the bridge to call
modifier onlyBridge() {
require(msg.sender == BRIDGE, "MyCustomL2Token: only bridge can mint and burn");
_;
Expand All @@ -39,33 +111,12 @@ contract MyCustomL2Token is IOptimismMintableERC20, ERC20 {
string memory _symbol
)
ERC20(_name, _symbol)
Semver(1, 0, 0)
{
REMOTE_TOKEN = _remoteToken;
BRIDGE = _bridge;
}

/// @custom:legacy
/// @notice Legacy getter for REMOTE_TOKEN.
function remoteToken() public view returns (address) {
return REMOTE_TOKEN;
}

/// @custom:legacy
/// @notice Legacy getter for BRIDGE.
function bridge() public view returns (address) {
return BRIDGE;
}

/// @notice ERC165 interface check function.
/// @param _interfaceId Interface ID to check.
/// @return Whether or not the interface is supported by this contract.
function supportsInterface(bytes4 _interfaceId) external pure virtual returns (bool) {
bytes4 iface1 = type(IERC165).interfaceId;
// Interface corresponding to the updated OptimismMintableERC20 (this contract).
bytes4 iface2 = type(IOptimismMintableERC20).interfaceId;
return _interfaceId == iface1 || _interfaceId == iface2;
}

/// @notice Allows the StandardBridge on this network to mint tokens.
/// @param _to Address to mint tokens to.
/// @param _amount Amount of tokens to mint.
Expand All @@ -75,23 +126,65 @@ contract MyCustomL2Token is IOptimismMintableERC20, ERC20 {
)
external
virtual
override(IOptimismMintableERC20)
override(IOptimismMintableERC20, ILegacyMintableERC20)
onlyBridge
{
_mint(_to, _amount);
emit Mint(_to, _amount);
}

/// @notice Prevents tokens from being withdrawn to L1.
/// @notice Burns tokens from an account.
/// @dev This function always reverts to prevent withdrawals to L1.
/// @param _from Address to burn tokens from.
/// @param _amount Amount of tokens to burn.
function burn(
address,
uint256
address _from,
uint256 _amount
)
external
virtual
override(IOptimismMintableERC20)
override(IOptimismMintableERC20, ILegacyMintableERC20)
onlyBridge
{
revert("MyCustomL2Token cannot be withdrawn");
// Instead of calling _burn(_from, _amount), we revert
// This makes it impossible to withdraw tokens back to L1
revert("MyCustomL2Token: withdrawals are not allowed");

// Note: The following line would normally execute but is unreachable
// _burn(_from, _amount);
// emit Burn(_from, _amount);
}

/// @notice ERC165 interface check function.
/// @param _interfaceId Interface ID to check.
/// @return Whether or not the interface is supported by this contract.
function supportsInterface(bytes4 _interfaceId) external pure virtual returns (bool) {
bytes4 iface1 = type(IERC165).interfaceId;
// Interface corresponding to the legacy L2StandardERC20
bytes4 iface2 = type(ILegacyMintableERC20).interfaceId;
// Interface corresponding to the updated OptimismMintableERC20
bytes4 iface3 = type(IOptimismMintableERC20).interfaceId;
return _interfaceId == iface1 || _interfaceId == iface2 || _interfaceId == iface3;
}

/// @notice Legacy getter for the remote token. Use REMOTE_TOKEN going forward.
function l1Token() public view override returns (address) {
return REMOTE_TOKEN;
}

/// @notice Legacy getter for the bridge. Use BRIDGE going forward.
function l2Bridge() public view override returns (address) {
return BRIDGE;
}

/// @notice Getter for REMOTE_TOKEN.
function remoteToken() public view override returns (address) {
return REMOTE_TOKEN;
}

/// @notice Getter for BRIDGE.
function bridge() public view override returns (address) {
return BRIDGE;
}
}
```