From 99d06337676c4a701c9e05c237b5730843e6f1a0 Mon Sep 17 00:00:00 2001 From: agusduha Date: Thu, 3 Oct 2024 15:15:22 -0300 Subject: [PATCH] feat: add crosschain erc20 interface --- packages/contracts-bedrock/semver-lock.json | 8 ++--- .../abi/OptimismSuperchainERC20.json | 36 +++++++++---------- .../src/L2/SuperchainERC20.sol | 13 +++---- .../src/L2/SuperchainERC20Bridge.sol | 4 +-- .../src/L2/interfaces/ICrosschainERC20.sol | 26 ++++++++++++++ .../interfaces/IOptimismSuperchainERC20.sol | 2 +- .../src/L2/interfaces/ISuperchainERC20.sol | 23 ++---------- .../test/L2/SuperchainERC20.t.sol | 31 ++++++++-------- .../test/L2/SuperchainERC20Bridge.t.sol | 2 +- .../fuzz/Protocol.unguided.t.sol | 4 +-- .../handlers/Protocol.t.sol | 2 +- 11 files changed, 80 insertions(+), 71 deletions(-) create mode 100644 packages/contracts-bedrock/src/L2/interfaces/ICrosschainERC20.sol diff --git a/packages/contracts-bedrock/semver-lock.json b/packages/contracts-bedrock/semver-lock.json index adae1ff092bf..c8b0991ed9fb 100644 --- a/packages/contracts-bedrock/semver-lock.json +++ b/packages/contracts-bedrock/semver-lock.json @@ -116,7 +116,7 @@ "sourceCodeHash": "0xfea53344596d735eff3be945ed1300dc75a6f8b7b2c02c0043af5b0036f5f239" }, "src/L2/OptimismSuperchainERC20.sol": { - "initCodeHash": "0xc6452d9aef6d76bdc789f3cddac6862658a481c619e6a2e7a74f6d61147f927b", + "initCodeHash": "0x964c826693c6633dc5eff6d4b059a30043775af46b06e42367aff91b904498da", "sourceCodeHash": "0x4463e49c98ceb3327bd768579341d1e0863c8c3925d4b533fbc0f7951306261f" }, "src/L2/OptimismSuperchainERC20Beacon.sol": { @@ -133,11 +133,11 @@ }, "src/L2/SuperchainERC20.sol": { "initCodeHash": "0xc5d2460186f7233c927e7db2dcc703c0e500b653ca82273b7bfad8045d85a470", - "sourceCodeHash": "0x9bc2e208774eb923894dbe391a5038a6189d7d36c202f4bf3e2c4dd332b0adf0" + "sourceCodeHash": "0x770764a897f76912ff5ad315ab7101f7777b8be47f21021d56cb9572b9efa458" }, "src/L2/SuperchainERC20Bridge.sol": { - "initCodeHash": "0xea7eb314f96cd2520a58012ff7cc376c82c5a95612187ff6bb96ace4f095ebc4", - "sourceCodeHash": "0x83188d878ce0b2890a7f7f41d09a8807f94a126e0ea274f0dac8b93f77217d3b" + "initCodeHash": "0xf85225ea25a87ba670b6ce0172a4814fda712d1c8a174fd4e8ce72b1cebcc2a0", + "sourceCodeHash": "0x66b56c0ac0d49b6da84da01a318f43418ef486e5fb40ae0af487568fde8566fb" }, "src/L2/SuperchainWETH.sol": { "initCodeHash": "0x5db03c5c4cd6ea9e4b3e74e28f50d04fd3e130af5109b34fa208808fa9ba7742", diff --git a/packages/contracts-bedrock/snapshots/abi/OptimismSuperchainERC20.json b/packages/contracts-bedrock/snapshots/abi/OptimismSuperchainERC20.json index 7c24b3fe0065..6c71001ad3a5 100644 --- a/packages/contracts-bedrock/snapshots/abi/OptimismSuperchainERC20.json +++ b/packages/contracts-bedrock/snapshots/abi/OptimismSuperchainERC20.json @@ -30,7 +30,7 @@ "type": "uint256" } ], - "name": "__superchainBurn", + "name": "__crosschainBurn", "outputs": [], "stateMutability": "nonpayable", "type": "function" @@ -48,7 +48,7 @@ "type": "uint256" } ], - "name": "__superchainMint", + "name": "__crosschainMint", "outputs": [], "stateMutability": "nonpayable", "type": "function" @@ -440,19 +440,6 @@ "name": "Burn", "type": "event" }, - { - "anonymous": false, - "inputs": [ - { - "indexed": false, - "internalType": "uint64", - "name": "version", - "type": "uint64" - } - ], - "name": "Initialized", - "type": "event" - }, { "anonymous": false, "inputs": [ @@ -469,7 +456,7 @@ "type": "uint256" } ], - "name": "Mint", + "name": "CrosschainBurnt", "type": "event" }, { @@ -488,7 +475,20 @@ "type": "uint256" } ], - "name": "SuperchainBurnt", + "name": "CrosschainMinted", + "type": "event" + }, + { + "anonymous": false, + "inputs": [ + { + "indexed": false, + "internalType": "uint64", + "name": "version", + "type": "uint64" + } + ], + "name": "Initialized", "type": "event" }, { @@ -507,7 +507,7 @@ "type": "uint256" } ], - "name": "SuperchainMinted", + "name": "Mint", "type": "event" }, { diff --git a/packages/contracts-bedrock/src/L2/SuperchainERC20.sol b/packages/contracts-bedrock/src/L2/SuperchainERC20.sol index 6c48b231baaf..0093806af701 100644 --- a/packages/contracts-bedrock/src/L2/SuperchainERC20.sol +++ b/packages/contracts-bedrock/src/L2/SuperchainERC20.sol @@ -1,7 +1,8 @@ // SPDX-License-Identifier: MIT pragma solidity 0.8.25; -import { ISuperchainERC20Extension } from "src/L2/interfaces/ISuperchainERC20.sol"; +import { ICrosschainERC20 } from "src/L2/interfaces/ICrosschainERC20.sol"; +import { ISuperchainERC20Errors } from "src/L2/interfaces/ISuperchainERC20.sol"; import { ISemver } from "src/universal/interfaces/ISemver.sol"; import { Predeploys } from "src/libraries/Predeploys.sol"; import { ERC20 } from "@solady/tokens/ERC20.sol"; @@ -10,7 +11,7 @@ import { ERC20 } from "@solady/tokens/ERC20.sol"; /// @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 SuperchainERC20Bridge to /// burn and mint tokens. -abstract contract SuperchainERC20 is ERC20, ISuperchainERC20Extension, ISemver { +abstract contract SuperchainERC20 is ERC20, ICrosschainERC20, ISuperchainERC20Errors, ISemver { /// @notice A modifier that only allows the SuperchainERC20Bridge to call modifier onlySuperchainERC20Bridge() { if (msg.sender != Predeploys.SUPERCHAIN_ERC20_BRIDGE) revert OnlySuperchainERC20Bridge(); @@ -26,18 +27,18 @@ abstract contract SuperchainERC20 is ERC20, ISuperchainERC20Extension, ISemver { /// @notice Allows the SuperchainERC20Bridge to mint tokens. /// @param _to Address to mint tokens to. /// @param _amount Amount of tokens to mint. - function __superchainMint(address _to, uint256 _amount) external virtual onlySuperchainERC20Bridge { + function __crosschainMint(address _to, uint256 _amount) external virtual onlySuperchainERC20Bridge { _mint(_to, _amount); - emit SuperchainMinted(_to, _amount); + emit CrosschainMinted(_to, _amount); } /// @notice Allows the SuperchainERC20Bridge to burn tokens. /// @param _from Address to burn tokens from. /// @param _amount Amount of tokens to burn. - function __superchainBurn(address _from, uint256 _amount) external virtual onlySuperchainERC20Bridge { + function __crosschainBurn(address _from, uint256 _amount) external virtual onlySuperchainERC20Bridge { _burn(_from, _amount); - emit SuperchainBurnt(_from, _amount); + emit CrosschainBurnt(_from, _amount); } } diff --git a/packages/contracts-bedrock/src/L2/SuperchainERC20Bridge.sol b/packages/contracts-bedrock/src/L2/SuperchainERC20Bridge.sol index 9d13de80f4ca..2046efb2d0f1 100644 --- a/packages/contracts-bedrock/src/L2/SuperchainERC20Bridge.sol +++ b/packages/contracts-bedrock/src/L2/SuperchainERC20Bridge.sol @@ -31,7 +31,7 @@ contract SuperchainERC20Bridge is ISuperchainERC20Bridge { function sendERC20(address _token, address _to, uint256 _amount, uint256 _chainId) external { if (_to == address(0)) revert ZeroAddress(); - ISuperchainERC20(_token).__superchainBurn(msg.sender, _amount); + ISuperchainERC20(_token).__crosschainBurn(msg.sender, _amount); bytes memory message = abi.encodeCall(this.relayERC20, (_token, msg.sender, _to, _amount)); IL2ToL2CrossDomainMessenger(MESSENGER).sendMessage(_chainId, address(this), message); @@ -53,7 +53,7 @@ contract SuperchainERC20Bridge is ISuperchainERC20Bridge { uint256 source = IL2ToL2CrossDomainMessenger(MESSENGER).crossDomainMessageSource(); - ISuperchainERC20(_token).__superchainMint(_to, _amount); + ISuperchainERC20(_token).__crosschainMint(_to, _amount); emit RelayERC20(_token, _from, _to, _amount, source); } diff --git a/packages/contracts-bedrock/src/L2/interfaces/ICrosschainERC20.sol b/packages/contracts-bedrock/src/L2/interfaces/ICrosschainERC20.sol new file mode 100644 index 000000000000..ba9af02f4cd2 --- /dev/null +++ b/packages/contracts-bedrock/src/L2/interfaces/ICrosschainERC20.sol @@ -0,0 +1,26 @@ +// SPDX-License-Identifier: MIT +pragma solidity ^0.8.0; + +/// @title ICrosschainERC20 +/// @notice This interface is a standard for crosschain ERC20 transfers. +interface ICrosschainERC20 { + /// @notice Emitted whenever tokens are minted by a crosschain transfer. + /// @param account Address of the account tokens are being minted for. + /// @param amount Amount of tokens minted. + event CrosschainMinted(address indexed account, uint256 amount); + + /// @notice Emitted whenever tokens are burned by a crosschain transfer. + /// @param account Address of the account tokens are being burned from. + /// @param amount Amount of tokens burned. + event CrosschainBurnt(address indexed account, uint256 amount); + + /// @notice Allows to mint tokens through a crosschain transfer. + /// @param _to Address to mint tokens to. + /// @param _amount Amount of tokens to mint. + function __crosschainMint(address _to, uint256 _amount) external; + + /// @notice Allows to burn tokens through a crosschain transfer. + /// @param _from Address to burn tokens from. + /// @param _amount Amount of tokens to burn. + function __crosschainBurn(address _from, uint256 _amount) external; +} diff --git a/packages/contracts-bedrock/src/L2/interfaces/IOptimismSuperchainERC20.sol b/packages/contracts-bedrock/src/L2/interfaces/IOptimismSuperchainERC20.sol index a887ecf0e030..dd32d14c1a4f 100644 --- a/packages/contracts-bedrock/src/L2/interfaces/IOptimismSuperchainERC20.sol +++ b/packages/contracts-bedrock/src/L2/interfaces/IOptimismSuperchainERC20.sol @@ -2,7 +2,7 @@ pragma solidity ^0.8.0; // Interfaces -import { ISuperchainERC20, ISuperchainERC20Extension } from "src/L2/interfaces/ISuperchainERC20.sol"; +import { ISuperchainERC20Extension } from "src/L2/interfaces/ISuperchainERC20.sol"; import { IERC20Solady } from "src/vendor/interfaces/IERC20Solady.sol"; /// @title IOptimismSuperchainERC20Errors diff --git a/packages/contracts-bedrock/src/L2/interfaces/ISuperchainERC20.sol b/packages/contracts-bedrock/src/L2/interfaces/ISuperchainERC20.sol index 47341c559719..f65e102de0e1 100644 --- a/packages/contracts-bedrock/src/L2/interfaces/ISuperchainERC20.sol +++ b/packages/contracts-bedrock/src/L2/interfaces/ISuperchainERC20.sol @@ -2,6 +2,7 @@ pragma solidity ^0.8.0; // Interfaces +import { ICrosschainERC20 } from "src/L2/interfaces/ICrosschainERC20.sol"; import { IERC20Solady } from "src/vendor/interfaces/IERC20Solady.sol"; /// @title ISuperchainERC20Errors @@ -13,27 +14,7 @@ interface ISuperchainERC20Errors { /// @title ISuperchainERC20Extension /// @notice This interface is available on the SuperchainERC20 contract. -interface ISuperchainERC20Extension is ISuperchainERC20Errors { - /// @notice Emitted whenever tokens are minted for by the SuperchainERC20Bridge. - /// @param account Address of the account tokens are being minted for. - /// @param amount Amount of tokens minted. - event SuperchainMinted(address indexed account, uint256 amount); - - /// @notice Emitted whenever tokens are burned by the SuperchainERC20Bridge. - /// @param account Address of the account tokens are being burned from. - /// @param amount Amount of tokens burned. - event SuperchainBurnt(address indexed account, uint256 amount); - - /// @notice Allows the SuperchainERC20Bridge to mint tokens. - /// @param _to Address to mint tokens to. - /// @param _amount Amount of tokens to mint. - function __superchainMint(address _to, uint256 _amount) external; - - /// @notice Allows the SuperchainERC20Bridge to burn tokens. - /// @param _from Address to burn tokens from. - /// @param _amount Amount of tokens to burn. - function __superchainBurn(address _from, uint256 _amount) external; -} +interface ISuperchainERC20Extension is ICrosschainERC20, ISuperchainERC20Errors { } /// @title ISuperchainERC20 /// @notice Combines Solady's ERC20 interface with the SuperchainERC20Extension interface. diff --git a/packages/contracts-bedrock/test/L2/SuperchainERC20.t.sol b/packages/contracts-bedrock/test/L2/SuperchainERC20.t.sol index 30b758a38e6d..eba6201c9756 100644 --- a/packages/contracts-bedrock/test/L2/SuperchainERC20.t.sol +++ b/packages/contracts-bedrock/test/L2/SuperchainERC20.t.sol @@ -16,7 +16,8 @@ import { IBeacon } from "@openzeppelin/contracts-v5/proxy/beacon/IBeacon.sol"; import { BeaconProxy } from "@openzeppelin/contracts-v5/proxy/beacon/BeaconProxy.sol"; // Target contract -import { SuperchainERC20, ISuperchainERC20Extension } from "src/L2/SuperchainERC20.sol"; +import { SuperchainERC20 } from "src/L2/SuperchainERC20.sol"; +import { ICrosschainERC20 } from "src/L2/interfaces/ICrosschainERC20.sol"; import { ISuperchainERC20Errors } from "src/L2/interfaces/ISuperchainERC20.sol"; import { SuperchainERC20Implementation_MockContract } from "test/mocks/SuperchainERC20Implementation.sol"; @@ -41,7 +42,7 @@ contract SuperchainERC20Test is Test { } /// @notice Tests the `mint` function reverts when the caller is not the bridge. - function testFuzz___superchainMint_callerNotBridge_reverts(address _caller, address _to, uint256 _amount) public { + function testFuzz___crosschainMint_callerNotBridge_reverts(address _caller, address _to, uint256 _amount) public { // Ensure the caller is not the bridge vm.assume(_caller != SUPERCHAIN_ERC20_BRIDGE); @@ -50,11 +51,11 @@ contract SuperchainERC20Test is Test { // Call the `mint` function with the non-bridge caller vm.prank(_caller); - superchainERC20.__superchainMint(_to, _amount); + superchainERC20.__crosschainMint(_to, _amount); } /// @notice Tests the `mint` succeeds and emits the `Mint` event. - function testFuzz___superchainMint_succeeds(address _to, uint256 _amount) public { + function testFuzz___crosschainMint_succeeds(address _to, uint256 _amount) public { // Ensure `_to` is not the zero address vm.assume(_to != ZERO_ADDRESS); @@ -66,13 +67,13 @@ contract SuperchainERC20Test is Test { vm.expectEmit(address(superchainERC20)); emit IERC20.Transfer(ZERO_ADDRESS, _to, _amount); - // Look for the emit of the `SuperchainMinted` event + // Look for the emit of the `CrosschainMinted` event vm.expectEmit(address(superchainERC20)); - emit ISuperchainERC20Extension.SuperchainMinted(_to, _amount); + emit ICrosschainERC20.CrosschainMinted(_to, _amount); // Call the `mint` function with the bridge caller vm.prank(SUPERCHAIN_ERC20_BRIDGE); - superchainERC20.__superchainMint(_to, _amount); + superchainERC20.__crosschainMint(_to, _amount); // Check the total supply and balance of `_to` after the mint were updated correctly assertEq(superchainERC20.totalSupply(), _totalSupplyBefore + _amount); @@ -80,7 +81,7 @@ contract SuperchainERC20Test is Test { } /// @notice Tests the `burn` function reverts when the caller is not the bridge. - function testFuzz___superchainBurn_callerNotBridge_reverts( + function testFuzz___crosschainBurn_callerNotBridge_reverts( address _caller, address _from, uint256 _amount @@ -95,17 +96,17 @@ contract SuperchainERC20Test is Test { // Call the `burn` function with the non-bridge caller vm.prank(_caller); - superchainERC20.__superchainBurn(_from, _amount); + superchainERC20.__crosschainBurn(_from, _amount); } - /// @notice Tests the `burn` burns the amount and emits the `SuperchainBurnt` event. - function testFuzz___superchainBurn_succeeds(address _from, uint256 _amount) public { + /// @notice Tests the `burn` burns the amount and emits the `CrosschainBurnt` event. + function testFuzz___crosschainBurn_succeeds(address _from, uint256 _amount) public { // Ensure `_from` is not the zero address vm.assume(_from != ZERO_ADDRESS); // Mint some tokens to `_from` so then they can be burned vm.prank(SUPERCHAIN_ERC20_BRIDGE); - superchainERC20.__superchainMint(_from, _amount); + superchainERC20.__crosschainMint(_from, _amount); // Get the total supply and balance of `_from` before the burn to compare later on the assertions uint256 _totalSupplyBefore = superchainERC20.totalSupply(); @@ -115,13 +116,13 @@ contract SuperchainERC20Test is Test { vm.expectEmit(address(superchainERC20)); emit IERC20.Transfer(_from, ZERO_ADDRESS, _amount); - // Look for the emit of the `SuperchainBurnt` event + // Look for the emit of the `CrosschainBurnt` event vm.expectEmit(address(superchainERC20)); - emit ISuperchainERC20Extension.SuperchainBurnt(_from, _amount); + emit ICrosschainERC20.CrosschainBurnt(_from, _amount); // Call the `burn` function with the bridge caller vm.prank(SUPERCHAIN_ERC20_BRIDGE); - superchainERC20.__superchainBurn(_from, _amount); + superchainERC20.__crosschainBurn(_from, _amount); // Check the total supply and balance of `_from` after the burn were updated correctly assertEq(superchainERC20.totalSupply(), _totalSupplyBefore - _amount); diff --git a/packages/contracts-bedrock/test/L2/SuperchainERC20Bridge.t.sol b/packages/contracts-bedrock/test/L2/SuperchainERC20Bridge.t.sol index 7ec72e508dad..fbeb75d77660 100644 --- a/packages/contracts-bedrock/test/L2/SuperchainERC20Bridge.t.sol +++ b/packages/contracts-bedrock/test/L2/SuperchainERC20Bridge.t.sol @@ -67,7 +67,7 @@ contract SuperchainERC20BridgeTest is Bridge_Initializer { // Mint some tokens to the sender so then they can be sent vm.prank(Predeploys.SUPERCHAIN_ERC20_BRIDGE); - superchainERC20.__superchainMint(_sender, _amount); + superchainERC20.__crosschainMint(_sender, _amount); // Get the total supply and balance of `_sender` before the send to compare later on the assertions uint256 _totalSupplyBefore = superchainERC20.totalSupply(); diff --git a/packages/contracts-bedrock/test/invariants/OptimismSuperchainERC20/fuzz/Protocol.unguided.t.sol b/packages/contracts-bedrock/test/invariants/OptimismSuperchainERC20/fuzz/Protocol.unguided.t.sol index db7c2243a1cd..2ead177276f8 100644 --- a/packages/contracts-bedrock/test/invariants/OptimismSuperchainERC20/fuzz/Protocol.unguided.t.sol +++ b/packages/contracts-bedrock/test/invariants/OptimismSuperchainERC20/fuzz/Protocol.unguided.t.sol @@ -17,7 +17,7 @@ contract ProtocolUnguided is ProtocolHandler, CompatibleAssert { bytes32 salt = MESSENGER.superTokenInitDeploySalts(token); amount = bound(amount, 0, type(uint256).max - OptimismSuperchainERC20(token).totalSupply()); vm.prank(sender); - try OptimismSuperchainERC20(token).__superchainMint(to, amount) { + try OptimismSuperchainERC20(token).__crosschainMint(to, amount) { compatibleAssert(sender == BRIDGE); (, uint256 currentValue) = ghost_totalSupplyAcrossChains.tryGet(salt); ghost_totalSupplyAcrossChains.set(salt, currentValue + amount); @@ -33,7 +33,7 @@ contract ProtocolUnguided is ProtocolHandler, CompatibleAssert { bytes32 salt = MESSENGER.superTokenInitDeploySalts(token); uint256 senderBalance = OptimismSuperchainERC20(token).balanceOf(sender); vm.prank(sender); - try OptimismSuperchainERC20(token).__superchainBurn(from, amount) { + try OptimismSuperchainERC20(token).__crosschainBurn(from, amount) { compatibleAssert(sender == BRIDGE); (, uint256 currentValue) = ghost_totalSupplyAcrossChains.tryGet(salt); ghost_totalSupplyAcrossChains.set(salt, currentValue - amount); diff --git a/packages/contracts-bedrock/test/invariants/OptimismSuperchainERC20/handlers/Protocol.t.sol b/packages/contracts-bedrock/test/invariants/OptimismSuperchainERC20/handlers/Protocol.t.sol index 9dbd5d5fc4a8..91aa9a81c619 100644 --- a/packages/contracts-bedrock/test/invariants/OptimismSuperchainERC20/handlers/Protocol.t.sol +++ b/packages/contracts-bedrock/test/invariants/OptimismSuperchainERC20/handlers/Protocol.t.sol @@ -79,7 +79,7 @@ contract ProtocolHandler is TestBase, StdUtils, Actors { index = bound(index, 0, allSuperTokens.length - 1); address addr = allSuperTokens[index]; vm.prank(BRIDGE); - OptimismSuperchainERC20(addr).__superchainMint(currentActor(), amount); + OptimismSuperchainERC20(addr).__crosschainMint(currentActor(), amount); // currentValue will be zero if key is not present (, uint256 currentValue) = ghost_totalSupplyAcrossChains.tryGet(MESSENGER.superTokenInitDeploySalts(addr)); ghost_totalSupplyAcrossChains.set(MESSENGER.superTokenInitDeploySalts(addr), currentValue + amount);