Skip to main content

Deploying your Custom ERC-20 token to Lisk

In this tutorial, you will learn how to bridge a custom ERC-20 token from Ethereum or Sepolia to the Lisk or Lisk Sepolia network, respectively. By 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 Lisk.

Learn step-by-step how you can create a custom token that conforms to the IOptimismMintableERC20 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 Deploying your Standard ERC-20 token to Lisk instead.

Prerequisites

note

You can deploy your Custom ERC-20 token on Lisk Mainnet by adopting the same process. For deploying to mainnet, ensure that your wallet has enough ETH.

The subsequent text contains commands for both Lisk and Lisk Sepolia for your ease. For more information, see the available Lisk networks and how to connect a wallet with them.

Get ETH on Sepolia and Lisk Sepolia

You will need to get some ETH on both, Sepolia and Lisk Sepolia networks.

info

You can use ETH Sepolia Faucet to get ETH on Sepolia. You can use the Superchain Faucet to get ETH on Lisk Sepolia.

Add Lisk Sepolia to Your Wallet

This tutorial uses Remix to deploy contracts. You will need to add the Lisk or Lisk Sepolia network to your wallet in order to follow this tutorial. Please follow the How to connect Lisk to a wallet guide, to connect your wallet to Lisk or Lisk Sepolia network.

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 Ethereum Mainnet or Sepolia, you can skip this step. For Sepolia, you can use the testing token located at 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 Lisk or Lisk Sepolia network. This tutorial uses Remix, so you can easily deploy a token without a framework like Hardhat or Foundry. 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.

1. Open Remix

Navigate to Remix in your browser.

2. 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, e.g. custom-token.sol.

3. Copy the example contract

Copy the following example contract into your new file:

custom-token.sol
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.0;

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.
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)
{
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.
function mint(
address _to,
uint256 _amount
)
external
virtual
override(IOptimismMintableERC20)
onlyBridge
{
_mint(_to, _amount);
emit Mint(_to, _amount);
}

/// @notice Prevents tokens from being withdrawn to L1.
function burn(
address,
uint256
)
external
virtual
override(IOptimismMintableERC20)
onlyBridge
{
revert("MyCustomL2Token cannot be withdrawn");
}
}

4. Review the example contract

Take a moment to review the example contract. It's almost the same as the standard OptimismMintableERC20 contract except that the _burn function has been made to always revert.

The contract for the custom token inherits from the IOptimismMintableERC20 interface and the ERC20 contract. The constructor takes the address of the L2 standard bridge, the address of the corresponding L1 token, the name of the ERC20 token, and the symbol of the ERC20 token. The mint function allows the bridge to mint tokens for users. 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, which is what we intend for this example.

/// @notice Prevents tokens from being withdrawn to L1.
function burn(
address,
uint256
)
external
virtual
override(IOptimismMintableERC20)
onlyBridge
{
revert("MyCustomL2Token cannot be withdrawn");
}

5. 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 pressing the blue "Compile" button.

6. Deploy the contract

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

_BRIDGE:      "0x4200000000000000000000000000000000000007"
_REMOTETOKEN: "<L1 ERC-20 address>"
_NAME: "My Custom Lisk L2 Token"
_SYMBOL: "MCL2T"
tip

If you used the testing token described in step Get an L1 ERC-20 Token Address, use the address 0x5589BB8228C07c4e15558875fAf2B859f678d129 for the _REMOTETOKEN parameter.