ERC20 Tokens with Zircuit Canonical Bridge

Zircuit's canonical bridge is derived from the bridge in the OP-stack framework. Therefore, the ERC20 tokens on Zircuit with their counterparts deployed on Ethereum mainnet follow the same architecture. If you would like to deploy an ERC20 token with no special logic, you can do so by using the prepared factory available on address 0x4200000000000000000000000000000000000012 on Zircuit.

In some cases, developers may desire to deploy ERC20 contracts with additional functionality that is not available in the templated contract that the factory creates, and they may want for these contracts to work with Zircuit's canonical bridge. This is certainly possible --- you may use any custom implementation provided that it meets the following conditions (you can reference the template implementation show below):

  • The token implements the IOptimismMintableERC20 interface together with EIP165, and reports that this interface is implemented. See function supportsInterface in the sample implementation below.

  • The canonical bridge is given the privilege to mint tokens to the users when they lock their tokens on L1 by calling the depositERC20 function on the L1StandardBridge contract, and to burn tokens from the users' accounts when they call bridgeERC20 function on the L2StandardBridge contract. See the modifier onlyBridge, and functions mint and burn in the reference implementation. Note that upon deposit, the bridge on L1 locks the respective amount of tokens and mints them to the user on L2. Upon withdrawal, it burns the respective number of tokens on L2 from the user's balance, and releases them on L1. The canonical bridge inherently assumes that the number of tokens in circulation on L2 matches the number of tokens locked in the bridge on L1. The developers should not violate this principle. This might happen by allowing parties other than the bridge to mint and burn tokens, or by implementing features such as rebasing, or fees on transfer.

  • The REMOTE_TOKEN, BRIDGE, and DECIMALS storage variables are properly initialized. The REMOTE_TOKEN address should match the token deployed on L1 as the bridge performs a check while bridging. The address of the L2StandardBridge on Zircuit is 0x4200000000000000000000000000000000000012.

Below is a template implementation of an ERC20 that can be further extended with custom logic.

// SPDX-License-Identifier: MIT
pragma solidity 0.8.20;

import { ERC20 } from "@openzeppelin/contracts/token/ERC20/ERC20.sol";
import { IERC165 } from "@openzeppelin/contracts/utils/introspection/IERC165.sol";

interface IOptimismMintableERC20 is IERC165 {
    function remoteToken() external view returns (address);

    function bridge() external returns (address);

    function mint(address _to, uint256 _amount) external;

    function burn(address _from, uint256 _amount) external;
}

contract MyZircuitERC20 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 Decimals of the token
    uint8 private immutable DECIMALS;

    /// @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, "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,
        uint8 _decimals
    )
        ERC20(_name, _symbol)
    {
        REMOTE_TOKEN = _remoteToken;
        BRIDGE = _bridge;
        DECIMALS = _decimals;
    }

    /// @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 Allows the StandardBridge on this network to burn tokens.
    /// @param _from   Address to burn tokens from.
    /// @param _amount Amount of tokens to burn.
    function burn(
        address _from,
        uint256 _amount
    )
        external
        virtual
        override(IOptimismMintableERC20)
        onlyBridge
    {
        _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 updated OptimismMintableERC20 (this contract).
        bytes4 iface3 = type(IOptimismMintableERC20).interfaceId;
        return _interfaceId == iface1 || _interfaceId == iface3;
    }

    /// @notice Getter for the remote token. Legacy, also use REMOTE_TOKEN
    function l1Token() public view returns (address) {
        return REMOTE_TOKEN;
    }

    /// @notice Getter for the bridge. Legacy, also use BRIDGE
    function l2Bridge() public view returns (address) {
        return BRIDGE;
    }

    /// @notice Getter for the remote token. Legacy, also use REMOTE_TOKEN
    function remoteToken() public view returns (address) {
        return REMOTE_TOKEN;
    }

    /// @notice Getter for the bridge. Legacy, also use BRIDGE.
    function bridge() public view returns (address) {
        return BRIDGE;
    }

    /// @dev Returns the number of decimals used to get its user representation.
    /// For example, if `decimals` equals `2`, a balance of `505` tokens should
    /// be displayed to a user as `5.05` (`505 / 10 ** 2`).
    /// NOTE: This information is only used for _display_ purposes: it in
    /// no way affects any of the arithmetic of the contract, including
    /// {IERC20-balanceOf} and {IERC20-transfer}.
    function decimals() public view override returns (uint8) {
        return DECIMALS;
    }
}

Last updated