Skip to content

Commit

Permalink
feat: implement IERC7802 (ethereum-optimism#12790)
Browse files Browse the repository at this point in the history
* feat: add IERC7802 (ethereum-optimism#123)

* feat: add IERC7802

* fix: token address fuzz error

* feat: remove ERC165 contract inheritance

* feat: add IERC20 interface support (ethereum-optimism#124)

* feat: add IERC20 interface support

* fix: interfaces tests
  • Loading branch information
agusduha authored and GrapeBaBa committed Nov 13, 2024
1 parent a23720f commit 4496563
Show file tree
Hide file tree
Showing 14 changed files with 145 additions and 36 deletions.
14 changes: 7 additions & 7 deletions packages/contracts-bedrock/semver-lock.json
Original file line number Diff line number Diff line change
Expand Up @@ -108,8 +108,8 @@
"sourceCodeHash": "0xa76133db7f449ae742f9ba988ad86ccb5672475f61298b9fefe411b63b63e9f6"
},
"src/L2/OptimismSuperchainERC20.sol": {
"initCodeHash": "0x24d85d246858d1aff78ae86c614dd0dc0f63b3326b2b662e3462c3a6f9b7965e",
"sourceCodeHash": "0xcb705d26e63e733051c8bd442ea69ce637a00c16d646ccc37b687b20941366fe"
"initCodeHash": "0x5bc5824030ecdb531e1f615d207cb73cdaa702e198769445d0ddbe717271eba9",
"sourceCodeHash": "0x0819c9411a155dca592d19b60c4176954202e4fe5d632a4ffbf88d465461252c"
},
"src/L2/OptimismSuperchainERC20Beacon.sol": {
"initCodeHash": "0x23dba3ceb9e58646695c306996c9e15251ac79acc6339c1a93d10a4c79da6dab",
Expand All @@ -125,15 +125,15 @@
},
"src/L2/SuperchainERC20.sol": {
"initCodeHash": "0xc5d2460186f7233c927e7db2dcc703c0e500b653ca82273b7bfad8045d85a470",
"sourceCodeHash": "0xba47f404e66e010ce417410476f26c704f2be4ce584cb79210bc5536a82ddb1f"
"sourceCodeHash": "0xcf39c16893cace1e7d61350bfff05a27f3ce8da8eb0ac02cb5ac7bf603f163fa"
},
"src/L2/SuperchainTokenBridge.sol": {
"initCodeHash": "0xef7590c30630a75f105384e339e52758569c25a5aa0a5934c521e004b8f86220",
"sourceCodeHash": "0x4f539e9d9096d31e861982b8f751fa2d7de0849590523375cf92e175294d1036"
"initCodeHash": "0x1cd2afdae6dd1b6ebc17f1d529e7d74c9b8b21b02db8589b8e389e2d5523d775",
"sourceCodeHash": "0x617aa994f659c5d8ebd54128d994f86f5b175ceca095b024b8524a7898e8ae62"
},
"src/L2/SuperchainWETH.sol": {
"initCodeHash": "0xc72cb486b815a65daa8bd5d0af8c965b6708cf8caf03de0a18023a63a6e6c604",
"sourceCodeHash": "0x39fff1d4702a2fec3dcba29c7f9604eabf20d32e9c5bf4377edeb620624aa467"
"initCodeHash": "0x5aef986a7c9c102b1e9b3068e2a2b66adce0a71dd5f39e03694622bf494f8d97",
"sourceCodeHash": "0xa62101a23b860e97f393027c898082a1c73d50679eceb6c6793844af29702359"
},
"src/L2/WETH.sol": {
"initCodeHash": "0x17ea1b1c5d5a622d51c2961fde886a5498de63584e654ed1d69ee80dddbe0b17",
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -153,6 +153,11 @@
"name": "InvalidCrossDomainSender",
"type": "error"
},
{
"inputs": [],
"name": "InvalidERC7802",
"type": "error"
},
{
"inputs": [],
"name": "Unauthorized",
Expand Down
19 changes: 19 additions & 0 deletions packages/contracts-bedrock/snapshots/abi/SuperchainWETH.json
Original file line number Diff line number Diff line change
Expand Up @@ -143,6 +143,25 @@
"stateMutability": "view",
"type": "function"
},
{
"inputs": [
{
"internalType": "bytes4",
"name": "_interfaceId",
"type": "bytes4"
}
],
"name": "supportsInterface",
"outputs": [
{
"internalType": "bool",
"name": "",
"type": "bool"
}
],
"stateMutability": "view",
"type": "function"
},
{
"inputs": [],
"name": "symbol",
Expand Down
7 changes: 3 additions & 4 deletions packages/contracts-bedrock/src/L2/OptimismSuperchainERC20.sol
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,6 @@ pragma solidity 0.8.25;

import { IOptimismSuperchainERC20 } from "src/L2/interfaces/IOptimismSuperchainERC20.sol";
import { Predeploys } from "src/libraries/Predeploys.sol";
import { ERC165 } from "@openzeppelin/contracts-v5/utils/introspection/ERC165.sol";
import { SuperchainERC20 } from "src/L2/SuperchainERC20.sol";
import { Initializable } from "@openzeppelin/contracts-v5/proxy/utils/Initializable.sol";
import { ZeroAddress, Unauthorized } from "src/libraries/errors/CommonErrors.sol";
Expand All @@ -16,7 +15,7 @@ import { ZeroAddress, Unauthorized } from "src/libraries/errors/CommonErrors.sol
/// OptimismSuperchainERC20 token, turning it fungible and interoperable across the superchain. Likewise, it
/// also enables the inverse conversion path.
/// Moreover, it builds on top of the L2ToL2CrossDomainMessenger for both replay protection and domain binding.
contract OptimismSuperchainERC20 is SuperchainERC20, Initializable, ERC165 {
contract OptimismSuperchainERC20 is SuperchainERC20, Initializable {
/// @notice Emitted whenever tokens are minted for an account.
/// @param to Address of the account tokens are being minted for.
/// @param amount Amount of tokens minted.
Expand Down Expand Up @@ -59,8 +58,8 @@ contract OptimismSuperchainERC20 is SuperchainERC20, Initializable, ERC165 {
}

/// @notice Semantic version.
/// @custom:semver 1.0.0-beta.8
string public constant override version = "1.0.0-beta.8";
/// @custom:semver 1.0.0-beta.9
string public constant override version = "1.0.0-beta.9";

/// @notice Constructs the OptimismSuperchainERC20 contract.
constructor() {
Expand Down
20 changes: 13 additions & 7 deletions packages/contracts-bedrock/src/L2/SuperchainERC20.sol
Original file line number Diff line number Diff line change
@@ -1,21 +1,21 @@
// SPDX-License-Identifier: MIT
pragma solidity 0.8.25;

import { ICrosschainERC20 } from "src/L2/interfaces/ICrosschainERC20.sol";
import { IERC7802, IERC165 } from "src/L2/interfaces/IERC7802.sol";
import { ISemver } from "src/universal/interfaces/ISemver.sol";
import { Predeploys } from "src/libraries/Predeploys.sol";
import { ERC20 } from "@solady-v0.0.245/tokens/ERC20.sol";
import { IERC20 } from "@openzeppelin/contracts/token/ERC20/IERC20.sol";
import { Unauthorized } from "src/libraries/errors/CommonErrors.sol";

/// @title SuperchainERC20
/// @notice SuperchainERC20 is a standard extension of the base ERC20 token contract that unifies ERC20 token
/// bridging to make it fungible across the Superchain. This construction allows the SuperchainTokenBridge to
/// burn and mint tokens.
abstract contract SuperchainERC20 is ERC20, ICrosschainERC20, ISemver {
/// @notice A standard ERC20 extension implementing IERC7802 for unified cross-chain fungibility across
/// the Superchain. Allows the SuperchainTokenBridge to mint and burn tokens as needed.
abstract contract SuperchainERC20 is ERC20, IERC7802, ISemver {
/// @notice Semantic version.
/// @custom:semver 1.0.0-beta.4
/// @custom:semver 1.0.0-beta.5
function version() external view virtual returns (string memory) {
return "1.0.0-beta.4";
return "1.0.0-beta.5";
}

/// @notice Allows the SuperchainTokenBridge to mint tokens.
Expand All @@ -39,4 +39,10 @@ abstract contract SuperchainERC20 is ERC20, ICrosschainERC20, ISemver {

emit CrosschainBurn(_from, _amount);
}

/// @inheritdoc IERC165
function supportsInterface(bytes4 _interfaceId) public view virtual returns (bool) {
return _interfaceId == type(IERC7802).interfaceId || _interfaceId == type(IERC20).interfaceId
|| _interfaceId == type(IERC165).interfaceId;
}
}
11 changes: 9 additions & 2 deletions packages/contracts-bedrock/src/L2/SuperchainTokenBridge.sol
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@ import { ZeroAddress, Unauthorized } from "src/libraries/errors/CommonErrors.sol

// Interfaces
import { ISuperchainERC20 } from "src/L2/interfaces/ISuperchainERC20.sol";
import { IERC7802, IERC165 } from "src/L2/interfaces/IERC7802.sol";
import { IL2ToL2CrossDomainMessenger } from "src/L2/interfaces/IL2ToL2CrossDomainMessenger.sol";

/// @custom:proxied true
Expand All @@ -20,6 +21,9 @@ contract SuperchainTokenBridge {
/// SuperchainTokenBridge.
error InvalidCrossDomainSender();

/// @notice Thrown when attempting to use a token that does not implement the ERC7802 interface.
error InvalidERC7802();

/// @notice Emitted when tokens are sent from one chain to another.
/// @param token Address of the token sent.
/// @param from Address of the sender.
Expand All @@ -42,8 +46,8 @@ contract SuperchainTokenBridge {
address internal constant MESSENGER = Predeploys.L2_TO_L2_CROSS_DOMAIN_MESSENGER;

/// @notice Semantic version.
/// @custom:semver 1.0.0-beta.2
string public constant version = "1.0.0-beta.2";
/// @custom:semver 1.0.0-beta.3
string public constant version = "1.0.0-beta.3";

/// @notice Sends tokens to a target address on another chain.
/// @dev Tokens are burned on the source chain.
Expand All @@ -63,6 +67,8 @@ contract SuperchainTokenBridge {
{
if (_to == address(0)) revert ZeroAddress();

if (!IERC165(_token).supportsInterface(type(IERC7802).interfaceId)) revert InvalidERC7802();

ISuperchainERC20(_token).crosschainBurn(msg.sender, _amount);

bytes memory message = abi.encodeCall(this.relayERC20, (_token, msg.sender, _to, _amount));
Expand All @@ -82,6 +88,7 @@ contract SuperchainTokenBridge {

(address crossDomainMessageSender, uint256 source) =
IL2ToL2CrossDomainMessenger(MESSENGER).crossDomainMessageContext();

if (crossDomainMessageSender != address(this)) revert InvalidCrossDomainSender();

ISuperchainERC20(_token).crosschainMint(_to, _amount);
Expand Down
15 changes: 11 additions & 4 deletions packages/contracts-bedrock/src/L2/SuperchainWETH.sol
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,8 @@ import { Preinstalls } from "src/libraries/Preinstalls.sol";
import { ISemver } from "src/universal/interfaces/ISemver.sol";
import { IL1Block } from "src/L2/interfaces/IL1Block.sol";
import { IETHLiquidity } from "src/L2/interfaces/IETHLiquidity.sol";
import { ICrosschainERC20 } from "src/L2/interfaces/ICrosschainERC20.sol";
import { IERC7802, IERC165 } from "src/L2/interfaces/IERC7802.sol";
import { IERC20 } from "@openzeppelin/contracts/token/ERC20/IERC20.sol";
import { Unauthorized, NotCustomGasToken } from "src/libraries/errors/CommonErrors.sol";

/// @custom:proxied true
Expand All @@ -21,10 +22,10 @@ import { Unauthorized, NotCustomGasToken } from "src/libraries/errors/CommonErro
/// @notice SuperchainWETH is a version of WETH that can be freely transfrered between chains
/// within the superchain. SuperchainWETH can be converted into native ETH on chains that
/// do not use a custom gas token.
contract SuperchainWETH is WETH98, ICrosschainERC20, ISemver {
contract SuperchainWETH is WETH98, IERC7802, ISemver {
/// @notice Semantic version.
/// @custom:semver 1.0.0-beta.9
string public constant version = "1.0.0-beta.9";
/// @custom:semver 1.0.0-beta.10
string public constant version = "1.0.0-beta.10";

/// @inheritdoc WETH98
function deposit() public payable override {
Expand Down Expand Up @@ -91,4 +92,10 @@ contract SuperchainWETH is WETH98, ICrosschainERC20, ISemver {

emit CrosschainBurn(_from, _amount);
}

/// @inheritdoc IERC165
function supportsInterface(bytes4 _interfaceId) public view virtual returns (bool) {
return _interfaceId == type(IERC7802).interfaceId || _interfaceId == type(IERC20).interfaceId
|| _interfaceId == type(IERC165).interfaceId;
}
}
Original file line number Diff line number Diff line change
@@ -1,9 +1,11 @@
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.0;

/// @title ICrosschainERC20
import { IERC165 } from "@openzeppelin/contracts/interfaces/IERC165.sol";

/// @title IERC7802
/// @notice Defines the interface for crosschain ERC20 transfers.
interface ICrosschainERC20 {
interface IERC7802 is IERC165 {
/// @notice Emitted when a crosschain transfer mints tokens.
/// @param to Address of the account tokens are being minted for.
/// @param amount Amount of tokens minted.
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -2,15 +2,17 @@
pragma solidity ^0.8.0;

// Interfaces
import { ICrosschainERC20 } from "src/L2/interfaces/ICrosschainERC20.sol";
import { IERC7802 } from "src/L2/interfaces/IERC7802.sol";
import { IERC20Solady as IERC20 } from "src/vendor/interfaces/IERC20Solady.sol";
import { ISemver } from "src/universal/interfaces/ISemver.sol";

/// @title ISuperchainERC20
/// @notice This interface is available on the SuperchainERC20 contract.
/// @dev This interface is needed for the abstract SuperchainERC20 implementation but is not part of the standard
interface ISuperchainERC20 is ICrosschainERC20, IERC20, ISemver {
interface ISuperchainERC20 is IERC7802, IERC20, ISemver {
error Unauthorized();

function supportsInterface(bytes4 _interfaceId) external view returns (bool);

function __constructor__() external;
}
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@ interface ISuperchainTokenBridge is ISemver {
error ZeroAddress();
error Unauthorized();
error InvalidCrossDomainSender();
error InvalidERC7802();

event SendERC20(
address indexed token, address indexed from, address indexed to, uint256 amount, uint256 destination
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -2,15 +2,16 @@
pragma solidity ^0.8.0;

import { IWETH98 } from "src/universal/interfaces/IWETH98.sol";
import { ICrosschainERC20 } from "src/L2/interfaces/ICrosschainERC20.sol";
import { IERC7802 } from "src/L2/interfaces/IERC7802.sol";
import { ISemver } from "src/universal/interfaces/ISemver.sol";

interface ISuperchainWETH is IWETH98, ICrosschainERC20, ISemver {
interface ISuperchainWETH is IWETH98, IERC7802, ISemver {
error Unauthorized();
error NotCustomGasToken();

function balanceOf(address src) external view returns (uint256);
function withdraw(uint256 _amount) external;
function supportsInterface(bytes4 _interfaceId) external view returns (bool);

function __constructor__() external;
}
24 changes: 20 additions & 4 deletions packages/contracts-bedrock/test/L2/SuperchainERC20.t.sol
Original file line number Diff line number Diff line change
Expand Up @@ -6,11 +6,11 @@ import { Test } from "forge-std/Test.sol";

// Libraries
import { Predeploys } from "src/libraries/Predeploys.sol";
import { IERC20Solady as IERC20 } from "src/vendor/interfaces/IERC20Solady.sol";

// Target contract
import { SuperchainERC20 } from "src/L2/SuperchainERC20.sol";
import { ICrosschainERC20 } from "src/L2/interfaces/ICrosschainERC20.sol";
import { IERC7802, IERC165 } from "src/L2/interfaces/IERC7802.sol";
import { IERC20 } from "@openzeppelin/contracts/token/ERC20/IERC20.sol";
import { ISuperchainERC20 } from "src/L2/interfaces/ISuperchainERC20.sol";
import { MockSuperchainERC20Implementation } from "test/mocks/SuperchainERC20Implementation.sol";

Expand Down Expand Up @@ -62,7 +62,7 @@ contract SuperchainERC20Test is Test {

// Look for the emit of the `CrosschainMint` event
vm.expectEmit(address(superchainERC20));
emit ICrosschainERC20.CrosschainMint(_to, _amount);
emit IERC7802.CrosschainMint(_to, _amount);

// Call the `mint` function with the bridge caller
vm.prank(SUPERCHAIN_TOKEN_BRIDGE);
Expand Down Expand Up @@ -105,7 +105,7 @@ contract SuperchainERC20Test is Test {

// Look for the emit of the `CrosschainBurn` event
vm.expectEmit(address(superchainERC20));
emit ICrosschainERC20.CrosschainBurn(_from, _amount);
emit IERC7802.CrosschainBurn(_from, _amount);

// Call the `burn` function with the bridge caller
vm.prank(SUPERCHAIN_TOKEN_BRIDGE);
Expand All @@ -115,4 +115,20 @@ contract SuperchainERC20Test is Test {
assertEq(superchainERC20.totalSupply(), _totalSupplyBefore - _amount);
assertEq(superchainERC20.balanceOf(_from), _fromBalanceBefore - _amount);
}

/// @notice Tests that the `supportsInterface` function returns true for the `IERC7802` interface.
function test_supportInterface_succeeds() public view {
assertTrue(superchainERC20.supportsInterface(type(IERC165).interfaceId));
assertTrue(superchainERC20.supportsInterface(type(IERC7802).interfaceId));
assertTrue(superchainERC20.supportsInterface(type(IERC20).interfaceId));
}

/// @notice Tests that the `supportsInterface` function returns false for any other interface than the
/// `IERC7802` one.
function testFuzz_supportInterface_returnFalse(bytes4 _interfaceId) public view {
vm.assume(_interfaceId != type(IERC165).interfaceId);
vm.assume(_interfaceId != type(IERC7802).interfaceId);
vm.assume(_interfaceId != type(IERC20).interfaceId);
assertFalse(superchainERC20.supportsInterface(_interfaceId));
}
}
30 changes: 28 additions & 2 deletions packages/contracts-bedrock/test/L2/SuperchainTokenBridge.t.sol
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@ import { ISuperchainTokenBridge } from "src/L2/interfaces/ISuperchainTokenBridge
import { ISuperchainERC20 } from "src/L2/interfaces/ISuperchainERC20.sol";
import { IOptimismSuperchainERC20Factory } from "src/L2/interfaces/IOptimismSuperchainERC20Factory.sol";
import { IERC20 } from "@openzeppelin/contracts/interfaces/IERC20.sol";
import { IERC7802 } from "src/L2/interfaces/IERC7802.sol";

/// @title SuperchainTokenBridgeTest
/// @notice Contract for testing the SuperchainTokenBridge contract.
Expand Down Expand Up @@ -60,6 +61,32 @@ contract SuperchainTokenBridgeTest is Bridge_Initializer {
superchainTokenBridge.sendERC20(address(superchainERC20), ZERO_ADDRESS, _amount, _chainId);
}

/// @notice Tests the `sendERC20` function reverts when the `token` does not support the IERC7802 interface.
function testFuzz_sendERC20_notSupportedIERC7802_reverts(
address _token,
address _sender,
address _to,
uint256 _amount,
uint256 _chainId
)
public
{
vm.assume(_to != ZERO_ADDRESS);
assumeAddressIsNot(_token, AddressType.Precompile, AddressType.ForgeAddress);

// Mock the call over the `supportsInterface` function to return false
vm.mockCall(
_token, abi.encodeCall(ISuperchainERC20.supportsInterface, (type(IERC7802).interfaceId)), abi.encode(false)
);

// Expect the revert with `InvalidERC7802` selector
vm.expectRevert(ISuperchainTokenBridge.InvalidERC7802.selector);

// Call the `sendERC20` function
vm.prank(_sender);
superchainTokenBridge.sendERC20(_token, _to, _amount, _chainId);
}

/// @notice Tests the `sendERC20` function burns the sender tokens, sends the message, and emits the `SendERC20`
/// event.
function testFuzz_sendERC20_succeeds(
Expand Down Expand Up @@ -137,7 +164,6 @@ contract SuperchainTokenBridgeTest is Bridge_Initializer {
/// @notice Tests the `relayERC20` function reverts when the `crossDomainMessageSender` that sent the message is not
/// the same SuperchainTokenBridge.
function testFuzz_relayERC20_notCrossDomainSender_reverts(
address _token,
address _crossDomainMessageSender,
uint256 _source,
address _to,
Expand All @@ -159,7 +185,7 @@ contract SuperchainTokenBridgeTest is Bridge_Initializer {

// Call the `relayERC20` function with the sender caller
vm.prank(Predeploys.L2_TO_L2_CROSS_DOMAIN_MESSENGER);
superchainTokenBridge.relayERC20(_token, _crossDomainMessageSender, _to, _amount);
superchainTokenBridge.relayERC20(address(superchainERC20), _crossDomainMessageSender, _to, _amount);
}

/// @notice Tests the `relayERC20` mints the proper amount and emits the `RelayERC20` event.
Expand Down
Loading

0 comments on commit 4496563

Please sign in to comment.