Skip to content

Commit 44da3d8

Browse files
authored
Merge pull request #1527 from ethereum-optimism/custom-ERC-20
Bridging your custom ERC-20 tutorial update
2 parents 296c296 + 4509421 commit 44da3d8

File tree

2 files changed

+164
-68
lines changed

2 files changed

+164
-68
lines changed

pages/app-developers/tutorials/bridging/standard-bridge-custom-token.mdx

Lines changed: 38 additions & 35 deletions
Original file line numberDiff line numberDiff line change
@@ -18,23 +18,22 @@ is_imported_content: 'false'
1818
import { Callout, Steps } from 'nextra/components'
1919
import { WipCallout } from '@/components/WipCallout'
2020

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

24-
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.
23+
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.
2524
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.
2625

27-
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.
26+
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.
2827
A custom token allows you to do things like trigger extra logic whenever a token is deposited.
2928
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.
3029

3130
<Callout type="error">
32-
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.
31+
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.
3332
</Callout>
3433

3534
## About OptimismMintableERC20s
3635

37-
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.
36+
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.
3837
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.
3938
Your L2 token contract must implement this interface in order to be bridged using the Standard Bridge system.
4039
This tutorial will show you how to create a custom token that implements this interface.
@@ -50,8 +49,8 @@ This tutorial explains how to create a bridged ERC-20 token on OP Sepolia.
5049
You will need to get some ETH on both of these testnets.
5150

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

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

7877
<Steps>
78+
{<h3>Open Remix</h3>}
7979

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

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

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

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

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

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

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

96-
{<h3>Review the example contract</h3>}
96+
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:
9797

98-
Take a moment to review the example contract.
99-
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.
100-
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.
98+
The `burn` function has been modified to always revert, making it impossible to withdraw tokens back to L1.
10199

102-
```solidity file=<rootDir>/public/tutorials/standard-bridge-custom-token.sol#L85-L96 hash=7c8cdadf1bec4c76dafb5552d1a593fe
103-
```
100+
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:
104101

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

107-
Save the file to automatically compile the contract.
108-
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.
105+
{<h3>Compile the contract</h3>}
109106

110-
{<h3>Deploy the contract</h3>}
107+
Save the file to automatically compile the contract.
108+
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.
111109

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

116-
```text
117-
_BRIDGE: "0x4200000000000000000000000000000000000010"
118-
_REMOTETOKEN: "<L1 ERC-20 address>"
119-
_NAME: "My Custom L2 Token"
120-
_SYMBOL: "MCL2T"
121-
```
112+
{<h3>Deploy the contract</h3>}
122113

114+
Open the deployment tab (this looks like an Ethereum logo with an arrow pointing left).
115+
Make sure that your environment is set to "Injected Provider", your wallet is connected to OP Sepolia, and Remix has access to your wallet.
116+
Then, select the `MyCustomL2Token` contract from the deployment dropdown and deploy it with the following parameters:
117+
118+
```text
119+
_bridge: "0x4200000000000000000000000000000000000010" // L2 Standard Bridge address
120+
_remoteToken: "<L1 ERC-20 address>" // Your L1 token address
121+
_name: "My Custom L2 Token" // Your token name
122+
_symbol: "MCL2T" // Your token symbol
123+
```
124+
125+
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.
123126
</Steps>
124127

125128
## Bridge some tokens
Lines changed: 126 additions & 33 deletions
Original file line numberDiff line numberDiff line change
@@ -1,12 +1,84 @@
11
// SPDX-License-Identifier: MIT
2-
pragma solidity ^0.8.0;
2+
pragma solidity 0.8.20;
33

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

8-
contract MyCustomL2Token is IOptimismMintableERC20, ERC20 {
9-
/// @notice Address of the corresponding version of this token on the remote chain.
8+
/**
9+
* @title ILegacyMintableERC20
10+
* @notice Legacy interface for the StandardL2ERC20 contract.
11+
*/
12+
interface ILegacyMintableERC20 {
13+
function mint(address _to, uint256 _amount) external;
14+
function burn(address _from, uint256 _amount) external;
15+
16+
function l1Token() external view returns (address);
17+
function l2Bridge() external view returns (address);
18+
}
19+
20+
/**
21+
* @title IOptimismMintableERC20
22+
* @notice Interface for the OptimismMintableERC20 contract.
23+
*/
24+
interface IOptimismMintableERC20 {
25+
function remoteToken() external view returns (address);
26+
function bridge() external view returns (address);
27+
function mint(address _to, uint256 _amount) external;
28+
function burn(address _from, uint256 _amount) external;
29+
}
30+
31+
/**
32+
* @title Simplified Semver for tutorial
33+
* @notice Simple contract to track semantic versioning
34+
*/
35+
contract Semver {
36+
string public version;
37+
38+
// Simple function to convert uint to string for version numbers
39+
function toString(uint256 value) internal pure returns (string memory) {
40+
// This function handles numbers from 0 to 999 which is sufficient for versioning
41+
if (value == 0) {
42+
return "0";
43+
}
44+
45+
uint256 temp = value;
46+
uint256 digits;
47+
48+
while (temp != 0) {
49+
digits++;
50+
temp /= 10;
51+
}
52+
53+
bytes memory buffer = new bytes(digits);
54+
55+
while (value != 0) {
56+
digits -= 1;
57+
buffer[digits] = bytes1(uint8(48 + uint256(value % 10)));
58+
value /= 10;
59+
}
60+
61+
return string(buffer);
62+
}
63+
64+
constructor(uint256 major, uint256 minor, uint256 patch) {
65+
version = string(abi.encodePacked(
66+
toString(major),
67+
".",
68+
toString(minor),
69+
".",
70+
toString(patch)
71+
));
72+
}
73+
}
74+
75+
/**
76+
* @title MyCustomL2Token
77+
* @notice A custom L2 token based on OptimismMintableERC20 that can be deposited
78+
* from L1 to L2, but cannot be withdrawn from L2 to L1.
79+
*/
80+
contract MyCustomL2Token is IOptimismMintableERC20, ILegacyMintableERC20, ERC20, Semver {
81+
/// @notice Address of the corresponding token on the remote chain.
1082
address public immutable REMOTE_TOKEN;
1183

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

25-
/// @notice A modifier that only allows the bridge to call.
97+
/// @notice A modifier that only allows the bridge to call
2698
modifier onlyBridge() {
2799
require(msg.sender == BRIDGE, "MyCustomL2Token: only bridge can mint and burn");
28100
_;
@@ -39,33 +111,12 @@ contract MyCustomL2Token is IOptimismMintableERC20, ERC20 {
39111
string memory _symbol
40112
)
41113
ERC20(_name, _symbol)
114+
Semver(1, 0, 0)
42115
{
43116
REMOTE_TOKEN = _remoteToken;
44117
BRIDGE = _bridge;
45118
}
46119

47-
/// @custom:legacy
48-
/// @notice Legacy getter for REMOTE_TOKEN.
49-
function remoteToken() public view returns (address) {
50-
return REMOTE_TOKEN;
51-
}
52-
53-
/// @custom:legacy
54-
/// @notice Legacy getter for BRIDGE.
55-
function bridge() public view returns (address) {
56-
return BRIDGE;
57-
}
58-
59-
/// @notice ERC165 interface check function.
60-
/// @param _interfaceId Interface ID to check.
61-
/// @return Whether or not the interface is supported by this contract.
62-
function supportsInterface(bytes4 _interfaceId) external pure virtual returns (bool) {
63-
bytes4 iface1 = type(IERC165).interfaceId;
64-
// Interface corresponding to the updated OptimismMintableERC20 (this contract).
65-
bytes4 iface2 = type(IOptimismMintableERC20).interfaceId;
66-
return _interfaceId == iface1 || _interfaceId == iface2;
67-
}
68-
69120
/// @notice Allows the StandardBridge on this network to mint tokens.
70121
/// @param _to Address to mint tokens to.
71122
/// @param _amount Amount of tokens to mint.
@@ -75,23 +126,65 @@ contract MyCustomL2Token is IOptimismMintableERC20, ERC20 {
75126
)
76127
external
77128
virtual
78-
override(IOptimismMintableERC20)
129+
override(IOptimismMintableERC20, ILegacyMintableERC20)
79130
onlyBridge
80131
{
81132
_mint(_to, _amount);
82133
emit Mint(_to, _amount);
83134
}
84135

85-
/// @notice Prevents tokens from being withdrawn to L1.
136+
/// @notice Burns tokens from an account.
137+
/// @dev This function always reverts to prevent withdrawals to L1.
138+
/// @param _from Address to burn tokens from.
139+
/// @param _amount Amount of tokens to burn.
86140
function burn(
87-
address,
88-
uint256
141+
address _from,
142+
uint256 _amount
89143
)
90144
external
91145
virtual
92-
override(IOptimismMintableERC20)
146+
override(IOptimismMintableERC20, ILegacyMintableERC20)
93147
onlyBridge
94148
{
95-
revert("MyCustomL2Token cannot be withdrawn");
149+
// Instead of calling _burn(_from, _amount), we revert
150+
// This makes it impossible to withdraw tokens back to L1
151+
revert("MyCustomL2Token: withdrawals are not allowed");
152+
153+
// Note: The following line would normally execute but is unreachable
154+
// _burn(_from, _amount);
155+
// emit Burn(_from, _amount);
156+
}
157+
158+
/// @notice ERC165 interface check function.
159+
/// @param _interfaceId Interface ID to check.
160+
/// @return Whether or not the interface is supported by this contract.
161+
function supportsInterface(bytes4 _interfaceId) external pure virtual returns (bool) {
162+
bytes4 iface1 = type(IERC165).interfaceId;
163+
// Interface corresponding to the legacy L2StandardERC20
164+
bytes4 iface2 = type(ILegacyMintableERC20).interfaceId;
165+
// Interface corresponding to the updated OptimismMintableERC20
166+
bytes4 iface3 = type(IOptimismMintableERC20).interfaceId;
167+
return _interfaceId == iface1 || _interfaceId == iface2 || _interfaceId == iface3;
168+
}
169+
170+
/// @notice Legacy getter for the remote token. Use REMOTE_TOKEN going forward.
171+
function l1Token() public view override returns (address) {
172+
return REMOTE_TOKEN;
173+
}
174+
175+
/// @notice Legacy getter for the bridge. Use BRIDGE going forward.
176+
function l2Bridge() public view override returns (address) {
177+
return BRIDGE;
178+
}
179+
180+
/// @notice Getter for REMOTE_TOKEN.
181+
function remoteToken() public view override returns (address) {
182+
return REMOTE_TOKEN;
183+
}
184+
185+
/// @notice Getter for BRIDGE.
186+
function bridge() public view override returns (address) {
187+
return BRIDGE;
96188
}
97189
}
190+
```

0 commit comments

Comments
 (0)