Skip to content

Commit

Permalink
feat: Add example of how to make existing ERC20 compatible with super…
Browse files Browse the repository at this point in the history
…chain interop
  • Loading branch information
tremarkley committed Nov 23, 2024
1 parent 0d36a50 commit 6925d7b
Show file tree
Hide file tree
Showing 2 changed files with 166 additions and 0 deletions.
87 changes: 87 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,7 @@
- [Prerequisites](#prerequisites)
- [Running the Tests](#running-the-tests)
- [🌉 Example: How to bridge a SuperchainERC20 token to another chain](#-example-how-to-bridge-a-superchainerc20-token-to-another-chain)
- [Updating an ERC20 contract to be interoperable](#updating-an-erc20-contract-to-be-interoperable)
- [🤝 Contributing](#-contributing)
- [⚖️ License](#-license)

Expand Down Expand Up @@ -227,6 +228,92 @@ Verify that the balance for the recipient of the `L2NativeSuperchainERC20` on ch
cast balance --erc20 <deployed-token-address> <recipient-address> --rpc-url http://127.0.0.1:9546
```

## Updating an ERC20 contract to be interoperable

This section explains how to modify an existing `ERC20` contract to make it interoperable within the Superchain.

In this example, we will update the [GovernanceToken](https://github.com/ethereum-optimism/optimism/blob/80465cd6c428d13166fce351741492f354621643/packages/contracts-bedrock/src/governance/GovernanceToken.sol) `ERC20` to make it interoperable. To do this we just need to implement the [IERC7802](https://ethereum-magicians.org/t/erc-7802-crosschain-token-interface/21508/2) interface.

**Note:** The `IERC7802` interface is designed to be **minimal** and only specifies the required function signatures (`crosschainMint` and `crosschainBurn`) and events (`CrosschainMint` and `CrosschainBurn`). **Developers have complete control over how these functions are implemented**, including the logic for handling cross-chain minting and burning. This allows flexibility to tailor the behavior to specific use cases or custom bridge solutions. In the example below we show how this interface is implemented for an example token that uses the [`SuperchainTokenBridge`](https://github.com/ethereum-optimism/optimism/blob/develop/packages/contracts-bedrock/src/L2/SuperchainTokenBridge.sol) for cross-chain minting and burning.

**1. Add the IERC7802 interface to the contract**

Import the `IERC7802` and update your contract to inherit from it.

```solidity
import { IERC7802, IERC165 } from "@contracts-bedrock/L2/interfaces/IERC7802.sol";
contract GovernanceToken is IERC7802, ERC20Burnable, ERC20Votes, Ownable {
// Contract implementation here
}
```

**2. Add the `crosschainMint` function to the contract**

The `crosschainMint` function defines how tokens are minted on the destination chain during a cross-chain transfer. While the interface requires the function's signature, **the implementation is entirely up to you.**

In this example, only the `SuperchainTokenBridge` contract will have permissions to mint the token during a crosschain transfer. If you’re using a custom bridge, you could replace `SuperchainTokenBridge` with your bridge contract and implement the desired logic. You can customize this function to add any logic or access control as needed.

```solidity
/// @notice Allows the SuperchainTokenBridge to mint tokens.
/// @param _to Address to mint tokens to.
/// @param _amount Amount of tokens to mint.
function crosschainMint(address _to, uint256 _amount) external {
// Only the `SuperchainTokenBridge` has permissions to mint tokens during crosschain transfers.
if (msg.sender != Predeploys.SUPERCHAIN_TOKEN_BRIDGE) revert Unauthorized();
// Mint tokens to the `_to` account's balance.
_mint(_to, _amount);
// Emit the CrosschainMint event included on IERC7802 for tracking token mints associated with cross chain transfers.
emit CrosschainMint(_to, _amount, msg.sender);
}
```

**3. Add the `crosschainBurn` function to the contract**
The `crosschainBurn` function defines how tokens are burned on the source chain during a cross-chain transfer. Again, while the interface specifies the function signature, **the implementation is completely flexible.**

In this example, only the `SuperchainTokenBridge` contract will have permissions to burn the token during a crosschain transfer. However, you can modify this to fit your requirements.

```solidity
/// @notice Allows the SuperchainTokenBridge to burn tokens.
/// @param _from Address to burn tokens from.
/// @param _amount Amount of tokens to burn.
function crosschainBurn(address _from, uint256 _amount) external {
// Only the `SuperchainTokenBridge` has permissions to burn tokens during crosschain transfers.
if (msg.sender != Predeploys.SUPERCHAIN_TOKEN_BRIDGE) revert Unauthorized();
// Burn the tokens from the `_from` account's balance.
_burn(_from, _amount);
// Emit the CrosschainBurn event included on IERC7802 for tracking token burns associated with cross chain transfers.
emit CrosschainBurn(_from, _amount, msg.sender);
}
```

**4. Add the `supportsInterface` function to the contract**
Since `IERC7802` is an extension of [IERC165](https://eips.ethereum.org/EIPS/eip-165), your contract must implement the `supportsInterface` function to indicate which interfaces it supports.

In this example our token supports `IERC7802`, `IERC20`, and `IERC165`

```solidity
/// @notice Query if a contract implements an interface
/// @param interfaceID The interface identifier, as specified in ERC-165
/// @dev Interface identification is specified in ERC-165. This function
/// uses less than 30,000 gas.
/// @return `true` if the contract implements `interfaceID` and
/// `interfaceID` is not 0xffffffff, `false` otherwise
function supportsInterface(bytes4 _interfaceId) public view virtual returns (bool) {
return _interfaceId == type(IERC7802).interfaceId || _interfaceId == type(IERC20).interfaceId
|| _interfaceId == type(IERC165).interfaceId;
}
```

After following these four steps your token is ready to be interoperable! The full example contract is available [here](packages/contracts/src/examples/L2NativeInteroperableGovernanceToken.sol). This example showcases that the IERC7802 interface does not enforce any specific logic for handling cross-chain minting and burning. It only ensures that the required functions and events exist, leaving all implementation details entirely up to you. You can:
- Use any custom access control mechanisms for crosschainMint and crosschainBurn.
- Implement specific checks, restrictions, or business logic tailored to your token's requirements.
- Integrate the interface with your own bridge or use it with the `SuperchainTokenBridge`.

## 🤝 Contributing

Contributions are encouraged, but please open an issue before making any major changes to ensure your changes will be accepted.
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,79 @@
// SPDX-License-Identifier: MIT
pragma solidity 0.8.15;

import { ERC20 } from "@openzeppelin/contracts/token/ERC20/ERC20.sol";
import { IERC20 } from "@openzeppelin/contracts/token/ERC20/IERC20.sol";
import { ERC20Burnable } from "@openzeppelin/contracts/token/ERC20/extensions/ERC20Burnable.sol";
import { ERC20Votes, ERC20Permit } from "@openzeppelin/contracts/token/ERC20/extensions/ERC20Votes.sol";
import { Ownable } from "@openzeppelin/contracts/access/Ownable.sol";
import { IERC7802, IERC165 } from "@contracts-bedrock/L2/interfaces/IERC7802.sol";
import { Predeploys } from "@contracts-bedrock/libraries/Predeploys.sol";
import { Unauthorized } from "@contracts-bedrock/libraries/errors/CommonErrors.sol";

contract GovernanceToken is IERC7802, ERC20Burnable, ERC20Votes, Ownable {
/// @notice Constructs the GovernanceToken contract.
constructor() ERC20("Optimism", "OP") ERC20Permit("Optimism") { }

/// @notice Allows the owner to mint tokens.
/// @param _account The account receiving minted tokens.
/// @param _amount The amount of tokens to mint.
function mint(address _account, uint256 _amount) public onlyOwner {
_mint(_account, _amount);
}

/// @notice Callback called after a token transfer.
/// @param from The account sending tokens.
/// @param to The account receiving tokens.
/// @param amount The amount of tokens being transfered.
function _afterTokenTransfer(address from, address to, uint256 amount) internal override(ERC20, ERC20Votes) {
super._afterTokenTransfer(from, to, amount);
}

/// @notice Internal mint function.
/// @param to The account receiving minted tokens.
/// @param amount The amount of tokens to mint.
function _mint(address to, uint256 amount) internal override(ERC20, ERC20Votes) {
super._mint(to, amount);
}

/// @notice Internal burn function.
/// @param account The account that tokens will be burned from.
/// @param amount The amount of tokens that will be burned.
function _burn(address account, uint256 amount) internal override(ERC20, ERC20Votes) {
super._burn(account, amount);
}

/// @notice Allows the SuperchainTokenBridge to mint tokens.
/// @param _to Address to mint tokens to.
/// @param _amount Amount of tokens to mint.
function crosschainMint(address _to, uint256 _amount) external {
// Only the `SuperchainTokenBridge` has permissions to mint tokens during crosschain transfers.
if (msg.sender != Predeploys.SUPERCHAIN_TOKEN_BRIDGE) revert Unauthorized();

// Mint tokens to the `_to` account's balance.
_mint(_to, _amount);

// Emit the CrosschainMint event included on IERC7802 for tracking token mints associated with cross chain transfers.
emit CrosschainMint(_to, _amount, msg.sender);
}

/// @notice Allows the SuperchainTokenBridge to burn tokens.
/// @param _from Address to burn tokens from.
/// @param _amount Amount of tokens to burn.
function crosschainBurn(address _from, uint256 _amount) external {
// Only the `SuperchainTokenBridge` has permissions to burn tokens during crosschain transfers.
if (msg.sender != Predeploys.SUPERCHAIN_TOKEN_BRIDGE) revert Unauthorized();

// Burn the tokens from the `_from` account's balance.
_burn(_from, _amount);

// Emit the CrosschainBurn event included on IERC7802 for tracking token burns associated with cross chain transfers.
emit CrosschainBurn(_from, _amount, msg.sender);
}

/// @inheritdoc IERC165
function supportsInterface(bytes4 _interfaceId) public view virtual returns (bool) {
return _interfaceId == type(IERC7802).interfaceId || _interfaceId == type(IERC20).interfaceId
|| _interfaceId == type(IERC165).interfaceId;
}
}

0 comments on commit 6925d7b

Please sign in to comment.