> ## Documentation Index
> Fetch the complete documentation index at: https://optimism-373f39ad-sdm-docs-revisit.mintlify.site/llms.txt
> Use this file to discover all available pages before exploring further.

# Create an L2 token for the Standard Bridge

> Learn how to create a standard or custom L2 representation of an L1 ERC-20 token that works with the Standard Bridge on an OP Stack chain.

export const TutorialFaucets = ({sepolia = true, opSepolia = true}) => <Info>
    {sepolia && <>
        You can use <a href="https://sepoliafaucet.com">this faucet</a> to get
        ETH on Sepolia.
      </>}
    {sepolia && opSepolia && " "}
    {opSepolia && <>
        You can use the{" "}
        <a href="https://console.optimism.io/faucet?utm_source=op-docs&amp;utm_medium=docs">
          Superchain Faucet
        </a>{" "}
        to get ETH on OP Sepolia.
      </>}
  </Info>;

export const networks_0 = "OP Sepolia"

In this tutorial, you'll create an L2 representation of an L1 ERC-20 token that works with the Standard Bridge system on an OP Stack chain.
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 layer 2.
By the end, you'll have an L2 token contract deployed on OP Sepolia that the Standard Bridge can mint and burn.
This tutorial does not cover the bridging itself: once your L2 token exists, follow [Bridging ERC-20 tokens with viem](./cross-dom-bridge-erc20) to move tokens between L1 and L2.

The Standard Bridge supports two kinds of L2 tokens, and this tutorial covers both:

* A **standard token** deployed through the [`OptimismMintableERC20Factory`](https://github.com/ethereum-optimism/optimism/blob/develop/packages/contracts-bedrock/src/universal/OptimismMintableERC20Factory.sol).
  Tokens created by this factory contract are compatible with the Standard Bridge system and include basic logic for deposits, transfers, and withdrawals.
  Choose this path if you don't need any specialized logic in your L2 token.
* A **custom token** that implements the [`IOptimismMintableERC20`](https://github.com/ethereum-optimism/optimism/blob/develop/packages/contracts-bedrock/interfaces/universal/IOptimismMintableERC20.sol) interface itself.
  A custom token allows you to do things like trigger extra logic whenever a token is deposited.
  Choose this path if you need specialized behavior like that.

Both paths share the same setup and fork at the [Create an L2 ERC-20 token](#create-an-l2-erc-20-token) section, where each path has its own tab.

<Info>
  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.
</Info>

## About OptimismMintableERC20s

The Standard Bridge system requires that L2 representations of L1 tokens implement the [`IOptimismMintableERC20`](https://github.com/ethereum-optimism/optimism/blob/develop/packages/contracts-bedrock/interfaces/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 deploy a basic standardized ERC-20 token through the [`OptimismMintableERC20Factory`](https://github.com/ethereum-optimism/optimism/blob/develop/packages/contracts-bedrock/src/universal/OptimismMintableERC20Factory.sol), or how to write a custom token that implements the interface itself.

## Dependencies

* [cast](https://book.getfoundry.sh/getting-started/installation), used on the standard-token path to deploy through the factory
* A web browser with a wallet extension, used on the custom-token path to deploy with [Remix](https://remix.ethereum.org)

## Get ETH on Sepolia and OP Sepolia

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.

<TutorialFaucets />

## Get an L1 ERC-20 token address

You will need an L1 ERC-20 token for this tutorial.
If you already have an L1 ERC-20 token deployed on Sepolia, you can skip this step.
Otherwise, you can use the testing token located at [`0x5589BB8228C07c4e15558875fAf2B859f678d129`](https://sepolia.etherscan.io/address/0x5589BB8228C07c4e15558875fAf2B859f678d129) that includes a `faucet()` function that can be used to mint tokens.

## Create an L2 ERC-20 token

Once you have an L1 ERC-20 token, you can create a corresponding L2 ERC-20 token on OP Sepolia.
Pick the tab that matches the kind of token you want to create.

<Tabs>
  <Tab title="Standard token">
    You can deploy your L2 ERC-20 token using the [`OptimismMintableERC20Factory`](https://github.com/ethereum-optimism/optimism/blob/develop/packages/contracts-bedrock/src/universal/OptimismMintableERC20Factory.sol).
    All tokens created by the factory implement the `IOptimismMintableERC20` interface and are compatible with the Standard Bridge system.

    <Steps>
      <Step title="Add a private key to your environment">
        You need a private key in order to sign transactions.
        Set your private key as an environment variable with the `export` command.
        Make sure this private key corresponds to an address that has ETH on {networks_0}.

        <Info>
          Want to create a new wallet for this tutorial?
          If you have [`cast`](https://book.getfoundry.sh/getting-started/installation) installed you can run `cast wallet new` in your terminal to create a new wallet and get the private key.
        </Info>

        ```bash theme={null}
        export TUTORIAL_PRIVATE_KEY=0x...
        ```
      </Step>

      <Step title="Add an OP Sepolia RPC URL to your environment">
        You'll need an RPC URL in order to connect to OP Sepolia.
        Set your RPC URL as an environment variable with the `export` command.

        ```bash theme={null}
        export TUTORIAL_RPC_URL=https://sepolia.optimism.io
        ```
      </Step>

      <Step title="Add your L1 ERC-20 token address to your environment">
        You'll need to know the address of your L1 ERC-20 token in order to create a bridged representation of it on OP Sepolia.
        Set your L1 ERC-20 token address as an environment variable with the `export` command.

        ```bash theme={null}
        # Replace this with your L1 ERC-20 token if not using the testing token!
        export TUTORIAL_L1_ERC20_ADDRESS=0x5589BB8228C07c4e15558875fAf2B859f678d129
        ```
      </Step>

      <Step title="Deploy your L2 ERC-20 token">
        You can now deploy your L2 ERC-20 token using the [`OptimismMintableERC20Factory`](https://github.com/ethereum-optimism/optimism/blob/develop/packages/contracts-bedrock/src/universal/OptimismMintableERC20Factory.sol).
        Use the `cast` command to trigger the deployment function on the factory contract.
        This example command creates a token with the name "My Standard Demo Token" and the symbol "L2TKN".
        The resulting L2 ERC-20 token address is printed to the console.

        ```bash theme={null}
        cast send 0x4200000000000000000000000000000000000012 "createOptimismMintableERC20(address,string,string)" $TUTORIAL_L1_ERC20_ADDRESS "My Standard Demo Token" "L2TKN" --private-key $TUTORIAL_PRIVATE_KEY --rpc-url $TUTORIAL_RPC_URL --json | jq -r '.logs[0].topics[2]' | cast parse-bytes32-address
        ```
      </Step>
    </Steps>
  </Tab>

  <Tab title="Custom token">
    This path uses [Remix](https://remix.ethereum.org) so you can easily deploy a token without a framework like [Hardhat](https://hardhat.org) or [Foundry](https://getfoundry.sh).
    You can follow the same general process within your favorite framework if you prefer.

    In this section, you'll be creating an ERC-20 token that can be deposited but cannot be withdrawn.
    This is just one example of the endless ways in which you could customize your L2 token.

    <Steps>
      <Step title="Add OP Sepolia to your wallet">
        You will need to add the OP Sepolia network to your wallet in order to follow this path.
        You can use [this website](https://chainid.link?network=op-sepolia) to connect your wallet to OP Sepolia.
      </Step>

      <Step title="Open Remix">
        Navigate to [Remix](https://remix.ethereum.org) in your browser.
      </Step>

      <Step title="Create a new file">
        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`.
      </Step>

      <Step title="Copy the example contract">
        Copy the following example contract into your new file:

        ```solidity theme={null}
        // SPDX-License-Identifier: MIT
        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";

        /**
        * @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.
        address public immutable BRIDGE;

        /// @notice Emitted whenever tokens are minted for an account.
        /// @param account Address of the account tokens are being minted for.
        /// @param amount  Amount of tokens minted.
        event Mint(address indexed account, uint256 amount);

        /// @notice Emitted whenever tokens are burned from an account.
        /// @param account Address of the account tokens are being burned from.
        /// @param amount  Amount of tokens burned.
        event Burn(address indexed account, uint256 amount);

        /// @notice A modifier that only allows the bridge to call
        modifier onlyBridge() {
            require(msg.sender == BRIDGE, "MyCustomL2Token: only bridge can mint and burn");
            _;
        }

        /// @param _bridge      Address of the L2 standard bridge.
        /// @param _remoteToken Address of the corresponding L1 token.
        /// @param _name        ERC20 name.
        /// @param _symbol      ERC20 symbol.
        constructor(
            address _bridge,
            address _remoteToken,
            string memory _name,
            string memory _symbol
        )
            ERC20(_name, _symbol)
            Semver(1, 0, 0)
        {
            REMOTE_TOKEN = _remoteToken;
            BRIDGE = _bridge;
        }

        /// @notice Allows the StandardBridge on this network to mint tokens.
        /// @param _to     Address to mint tokens to.
        /// @param _amount Amount of tokens to mint.
        function mint(
            address _to,
            uint256 _amount
        )
            external
            virtual
            override(IOptimismMintableERC20, ILegacyMintableERC20)
            onlyBridge
        {
            _mint(_to, _amount);
            emit Mint(_to, _amount);
        }

        /// @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 _from,
            uint256 _amount
        )
            external
            virtual
            override(IOptimismMintableERC20, ILegacyMintableERC20)
            onlyBridge
        {
            // 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;
        }
        }
        ```
      </Step>

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

        The `burn` function has been modified to always revert, making it impossible to withdraw tokens back to L1.

        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:

        ```solidity theme={null}
        /// @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 _from,
            uint256 _amount
        )
            external
            virtual
            override(IOptimismMintableERC20, ILegacyMintableERC20)
            onlyBridge
        {
            // 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);
        }
        ```
      </Step>

      <Step title="Compile the contract">
        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.

        Make sure you're using Solidity compiler version 0.8.20, the exact version the example contract's `pragma solidity 0.8.20;` requires.
      </Step>

      <Step title="Deploy the contract">
        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 theme={null}
        _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.
      </Step>
    </Steps>
  </Tab>
</Tabs>

## Bridge some tokens

Now that you have an L2 ERC-20 token, you can bridge some tokens from L1 to L2.
Check out the tutorial on [Bridging ERC-20 tokens with viem](./cross-dom-bridge-erc20) to learn how to bridge your L1 ERC-20 to L2s using viem.
If you deployed the custom example token from this tutorial, remember that the withdrawal step will *not* work for it: the example contract's `burn` function always reverts, which is exactly what that example was meant to demonstrate.

## Add to the Superchain Token List

The [Superchain Token List](https://github.com/ethereum-optimism/ethereum-optimism.github.io#readme) is a common list of tokens deployed on chains within the Optimism Superchain.
This list is used by services like the [Superchain Bridges UI](https://app.optimism.io/bridge?utm_source=op-docs\&utm_medium=docs).
If you want your OP Mainnet token to be included in this list, take a look at the [review process and merge criteria](https://github.com/ethereum-optimism/ethereum-optimism.github.io#review-process-and-merge-criteria).
