From 4394925ea2a0f0457c381a9482e16b7c02cd0fc2 Mon Sep 17 00:00:00 2001 From: agusduha Date: Thu, 25 Jul 2024 10:40:33 -0300 Subject: [PATCH 01/18] feat: introduce OptimismSuperchainERC20 --- .../src/L2/OptimismSuperchainERC20.sol | 166 +++++++++ .../test/L2/OptimismSuperchainERC20.t.sol | 323 ++++++++++++++++++ 2 files changed, 489 insertions(+) create mode 100644 packages/contracts-bedrock/src/L2/OptimismSuperchainERC20.sol create mode 100644 packages/contracts-bedrock/test/L2/OptimismSuperchainERC20.t.sol diff --git a/packages/contracts-bedrock/src/L2/OptimismSuperchainERC20.sol b/packages/contracts-bedrock/src/L2/OptimismSuperchainERC20.sol new file mode 100644 index 000000000000..31878c2bf368 --- /dev/null +++ b/packages/contracts-bedrock/src/L2/OptimismSuperchainERC20.sol @@ -0,0 +1,166 @@ +// SPDX-License-Identifier: MIT +pragma solidity 0.8.25; + +// import { ERC20 } from "@openzeppelin/contracts/token/ERC20/ERC20.sol"; +import { ERC20 } from "@solady/tokens/ERC20.sol"; +import { IL2ToL2CrossDomainMessenger } from "src/L2/IL2ToL2CrossDomainMessenger.sol"; +import { ISemver } from "src/universal/ISemver.sol"; +import { SafeCall } from "src/libraries/SafeCall.sol"; +import { Predeploys } from "src/libraries/Predeploys.sol"; + +/// @notice Thrown when attempting to relay a message and the function caller (msg.sender) is not +/// L2ToL2CrossDomainMessenger. +error RelayMessageCallerNotL2ToL2CrossDomainMessenger(); + +/// @notice Thrown when attempting to relay a message and the cross domain message sender is not this +/// OptimismSuperchainERC20. +error MessageSenderNotThisSuperchainERC20(); + +/// @notice Thrown when attempting to mint or burn tokens and the function caller is not the StandardBridge. +error CallerNotBridge(); + +/// @notice Thrown when attempting to mint or burn tokens and the account is the zero address. +error ZeroAddress(); + +/// @custom:proxied +/// @title OptimismSuperchainERC20 +/// @notice OptimismSuperchainERC20 is a standard extension of the base ERC20 token contract that unifies ERC20 token +/// bridging to make it fungible across the Superchain. This construction builds on top of the +/// L2ToL2CrossDomainMessenger for both replay protection and domain binding. +contract OptimismSuperchainERC20 is ERC20, ISemver { + /// @notice Address of the corresponding version of this token on the remote chain. + address public immutable REMOTE_TOKEN; + + /// @notice Decimals of the token + uint8 private immutable DECIMALS; + + /// @notice Name of the token + string private _name; + + /// @notice Symbol of the token + string private _symbol; + + /// @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 Emitted whenever tokens are sent to another chain. + /// @param from Address of the sender. + /// @param to Address of the recipient. + /// @param amount Amount of tokens sent. + /// @param chainId Chain ID of the destination chain. + event SentERC20(address indexed from, address indexed to, uint256 amount, uint256 chainId); + + /// @notice Emitted whenever tokens are successfully relayed on this chain. + /// @param to Address of the recipient. + /// @param amount Amount of tokens relayed. + event RelayedERC20(address indexed to, uint256 amount); + + /// @notice A modifier that only allows the bridge to call + modifier onlyBridge() { + if (msg.sender != Predeploys.L2_STANDARD_BRIDGE) revert CallerNotBridge(); + _; + } + + /// @notice Semantic version. + /// @custom:semver 1.0.0-beta.1 + string public constant version = "1.0.0-beta.1"; + + /// @param _remoteToken Address of the corresponding remote token. + /// @param _tokenName ERC20 name. + /// @param _tokenSymbol ERC20 symbol. + /// @param _decimals ERC20 decimals. + constructor(address _remoteToken, string memory _tokenName, string memory _tokenSymbol, uint8 _decimals) { + REMOTE_TOKEN = _remoteToken; + DECIMALS = _decimals; + _name = _tokenName; + _symbol = _tokenSymbol; + } + + /// @notice Allows the StandardBridge 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 onlyBridge { + if (_to == address(0)) revert ZeroAddress(); + + _mint(_to, _amount); + + emit Mint(_to, _amount); + } + + /// @notice Allows the StandardBridge 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 onlyBridge { + if (_from == address(0)) revert ZeroAddress(); + + _burn(_from, _amount); + + emit Burn(_from, _amount); + } + + /// @notice Sends tokens to some target address on another chain. + /// @param _to Address to send tokens to. + /// @param _amount Amount of tokens to send. + /// @param _chainId Chain ID of the destination chain. + function sendERC20(address _to, uint256 _amount, uint256 _chainId) external { + if (_to == address(0)) revert ZeroAddress(); + + _burn(msg.sender, _amount); + + bytes memory _message = abi.encodeCall(this.relayERC20, (_to, _amount)); + IL2ToL2CrossDomainMessenger(Predeploys.L2_TO_L2_CROSS_DOMAIN_MESSENGER).sendMessage( + _chainId, address(this), _message + ); + + emit SentERC20(msg.sender, _to, _amount, _chainId); + } + + /// @notice Relays tokens received from another chain. + /// @param _to Address to relay tokens to. + /// @param _amount Amount of tokens to relay. + function relayERC20(address _to, uint256 _amount) external { + if (_to == address(0)) revert ZeroAddress(); + + if (msg.sender != Predeploys.L2_TO_L2_CROSS_DOMAIN_MESSENGER) { + revert RelayMessageCallerNotL2ToL2CrossDomainMessenger(); + } + + if ( + IL2ToL2CrossDomainMessenger(Predeploys.L2_TO_L2_CROSS_DOMAIN_MESSENGER).crossDomainMessageSender() + != address(this) + ) { + revert MessageSenderNotThisSuperchainERC20(); + } + + _mint(_to, _amount); + + emit RelayedERC20(_to, _amount); + } + + /// @notice 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; + } + + /// @notice Returns the name of the token. + function name() public view virtual override returns (string memory) { + return _name; + } + + /// @notice Returns the symbol of the token. + function symbol() public view virtual override returns (string memory) { + return _symbol; + } +} diff --git a/packages/contracts-bedrock/test/L2/OptimismSuperchainERC20.t.sol b/packages/contracts-bedrock/test/L2/OptimismSuperchainERC20.t.sol new file mode 100644 index 000000000000..76e47e70d9f1 --- /dev/null +++ b/packages/contracts-bedrock/test/L2/OptimismSuperchainERC20.t.sol @@ -0,0 +1,323 @@ +// SPDX-License-Identifier: MIT +pragma solidity 0.8.25; + +// Testing utilities +import { Test } from "forge-std/Test.sol"; + +// Libraries +import { Predeploys } from "src/libraries/Predeploys.sol"; +import { IERC20 } from "@openzeppelin/contracts/token/ERC20/IERC20.sol"; +import { stdStorage, StdStorage } from "forge-std/Test.sol"; +import { ERC20 } from "@openzeppelin/contracts/token/ERC20/ERC20.sol"; +import { IL2ToL2CrossDomainMessenger } from "src/L2/IL2ToL2CrossDomainMessenger.sol"; + +// Target contract +import { + OptimismSuperchainERC20, + CallerNotBridge, + RelayMessageCallerNotL2ToL2CrossDomainMessenger, + MessageSenderNotThisSuperchainERC20, + CallerNotBridge, + ZeroAddress +} from "src/L2/OptimismSuperchainERC20.sol"; + +/// @title OptimismSuperchainERC20Test +/// @notice Contract for testing the OptimismSuperchainERC20 contract. +contract OptimismSuperchainERC20Test is Test { + address internal constant ZERO_ADDRESS = address(0); + address internal constant REMOTE_TOKEN = address(0x123); + string internal constant NAME = "OptimismSuperchainERC20"; + string internal constant SYMBOL = "SCE"; + uint8 internal constant DECIMALS = 18; + address internal constant BRIDGE = Predeploys.L2_STANDARD_BRIDGE; + address internal constant MESSENGER = Predeploys.L2_TO_L2_CROSS_DOMAIN_MESSENGER; + + OptimismSuperchainERC20 public superchainERC20; + + /// @notice Sets up the test suite. + function setUp() public { + superchainERC20 = new OptimismSuperchainERC20(REMOTE_TOKEN, NAME, SYMBOL, DECIMALS); + } + + /// @notice Helper function to setup a mock and expect a call to it. + function _mockAndExpect(address _receiver, bytes memory _calldata, bytes memory _returned) internal { + vm.mockCall(_receiver, _calldata, _returned); + vm.expectCall(_receiver, _calldata); + } + + /// @notice Test that the bridge's constructor sets the correct values. + function test_constructor_succeeds() public view { + assertEq(superchainERC20.name(), NAME); + assertEq(superchainERC20.symbol(), SYMBOL); + assertEq(superchainERC20.decimals(), DECIMALS); + assertEq(superchainERC20.REMOTE_TOKEN(), REMOTE_TOKEN); + } + + /// @notice Tests the `mint` function reverts when the caller is not the bridge. + function testFuzz_mint_callerNotBridge_reverts(address _caller, address _to, uint256 _amount) public { + // Ensure the caller is not the bridge + vm.assume(_caller != BRIDGE); + + // Expect the revert with `CallerNotBridge` selector + vm.expectRevert(CallerNotBridge.selector); + + // Call the `mint` function with the non-bridge caller + vm.prank(_caller); + superchainERC20.mint(_to, _amount); + } + + /// @notice Tests the `mint` function reverts when the amount is zero. + function testFuzz_mint_zeroAddressTo_reverts(uint256 _amount) public { + // Expect the revert with `ZeroAddress` selector + vm.expectRevert(ZeroAddress.selector); + + // Call the `mint` function with the zero address + vm.prank(BRIDGE); + superchainERC20.mint({ _to: ZERO_ADDRESS, _amount: _amount }); + } + + /// @notice Tests the `mint` succeeds and emits the `Mint` event. + function testFuzz_mint_succeeds(address _to, uint256 _amount) public { + // Ensure `_to` is not the zero address + vm.assume(_to != ZERO_ADDRESS); + + // Get the total supply and balance of `_to` before the mint to compare later on the assertions + uint256 _totalSupplyBefore = superchainERC20.totalSupply(); + uint256 _toBalanceBefore = superchainERC20.balanceOf(_to); + + // Look for the emit of the `Transfer` event + vm.expectEmit(true, true, true, true, address(superchainERC20)); + emit IERC20.Transfer(ZERO_ADDRESS, _to, _amount); + + // Look for the emit of the `Mint` event + vm.expectEmit(true, true, true, true, address(superchainERC20)); + emit OptimismSuperchainERC20.Mint(_to, _amount); + + // Call the `mint` function with the bridge caller + vm.prank(BRIDGE); + superchainERC20.mint(_to, _amount); + + // Check the total supply and balance of `_to` after the mint were updated correctly + assertEq(superchainERC20.totalSupply(), _totalSupplyBefore + _amount); + assertEq(superchainERC20.balanceOf(_to), _toBalanceBefore + _amount); + } + + /// @notice Tests the `burn` function reverts when the caller is not the bridge. + function testFuzz_burn_callerNotBridge_reverts(address _caller, address _from, uint256 _amount) public { + // Ensure the caller is not the bridge + vm.assume(_caller != BRIDGE); + + // Expect the revert with `CallerNotBridge` selector + vm.expectRevert(CallerNotBridge.selector); + + // Call the `burn` function with the non-bridge caller + vm.prank(_caller); + superchainERC20.burn(_from, _amount); + } + + /// @notice Tests the `burn` function reverts when the amount is zero. + function testFuzz_burn_zeroAddressFrom_reverts(uint256 _amount) public { + // Expect the revert with `ZeroAddress` selector + vm.expectRevert(ZeroAddress.selector); + + // Call the `burn` function with the zero address + vm.prank(BRIDGE); + superchainERC20.burn({ _from: ZERO_ADDRESS, _amount: _amount }); + } + + /// @notice Tests the `burn` burns the amount and emits the `Burn` event. + function testFuzz_burn_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(BRIDGE); + superchainERC20.mint(_from, _amount); + + // Get the total supply and balance of `_from` before the burn to compare later on the assertions + uint256 _totalSupplyBefore = superchainERC20.totalSupply(); + uint256 _fromBalanceBefore = superchainERC20.balanceOf(_from); + + // Look for the emit of the `Transfer` event + vm.expectEmit(true, true, true, true, address(superchainERC20)); + emit IERC20.Transfer(_from, ZERO_ADDRESS, _amount); + + // Look for the emit of the `Burn` event + vm.expectEmit(true, true, true, true, address(superchainERC20)); + emit OptimismSuperchainERC20.Burn(_from, _amount); + + // Call the `burn` function with the bridge caller + vm.prank(BRIDGE); + superchainERC20.burn(_from, _amount); + + // Check the total supply and balance of `_from` after the burn were updated correctly + assertEq(superchainERC20.totalSupply(), _totalSupplyBefore - _amount); + assertEq(superchainERC20.balanceOf(_from), _fromBalanceBefore - _amount); + } + + /// @notice Tests the `sendERC20` function burns the sender tokens, sends the message, and emits the `SentERC20` + /// event. + function testFuzz_sendERC20_succeeds(address _sender, address _to, uint256 _amount, uint256 _chainId) external { + // Ensure `_sender` is not the zero address + vm.assume(_sender != ZERO_ADDRESS); + vm.assume(_to != ZERO_ADDRESS); + + // Mint some tokens to the sender so then they can be sent + vm.prank(BRIDGE); + superchainERC20.mint(_sender, _amount); + + // Get the total supply and balance of `_sender` before the send to compare later on the assertions + uint256 _totalSupplyBefore = superchainERC20.totalSupply(); + uint256 _senderBalanceBefore = superchainERC20.balanceOf(_sender); + + // Look for the emit of the `Transfer` event + vm.expectEmit(true, true, true, true, address(superchainERC20)); + emit IERC20.Transfer(_sender, ZERO_ADDRESS, _amount); + + // Look for the emit of the `SentERC20` event + vm.expectEmit(true, true, true, true, address(superchainERC20)); + emit OptimismSuperchainERC20.SentERC20(_sender, _to, _amount, _chainId); + + // Mock the call over the `sendMessage` function and expect it to be called properly + bytes memory _message = abi.encodeCall(superchainERC20.relayERC20, (_to, _amount)); + _mockAndExpect( + MESSENGER, + abi.encodeWithSelector( + IL2ToL2CrossDomainMessenger.sendMessage.selector, _chainId, address(superchainERC20), _message + ), + abi.encode("") + ); + + // Call the `sendERC20` function + vm.prank(_sender); + superchainERC20.sendERC20(_to, _amount, _chainId); + + // Check the total supply and balance of `_sender` after the send were updated correctly + assertEq(superchainERC20.totalSupply(), _totalSupplyBefore - _amount); + assertEq(superchainERC20.balanceOf(_sender), _senderBalanceBefore - _amount); + } + + /// @notice Tests the `sendERC20` function reverts when the `_to` address is the zero address. + function testFuzz_sendERC20_zeroAddressTo_reverts(uint256 _amount, uint256 _chainId) public { + // Expect the revert with `ZeroAddress` selector + vm.expectRevert(ZeroAddress.selector); + + // Call the `sendERC20` function with the zero address + vm.prank(BRIDGE); + superchainERC20.sendERC20({ _to: ZERO_ADDRESS, _amount: _amount, _chainId: _chainId }); + } + + /// @notice Tests the `relayERC20` function reverts when the caller is not the L2ToL2CrossDomainMessenger. + function testFuzz_relayERC20_notMessenger_reverts(address _caller, address _to, uint256 _amount) public { + // Ensure the caller is not the messenger + vm.assume(_caller != MESSENGER); + vm.assume(_to != ZERO_ADDRESS); + + // Expect the revert with `RelayMessageCallerNotL2ToL2CrossDomainMessenger` selector + vm.expectRevert(RelayMessageCallerNotL2ToL2CrossDomainMessenger.selector); + + // Call the `relayERC20` function with the non-messenger caller + vm.prank(_caller); + superchainERC20.relayERC20(_to, _amount); + } + + /// @notice Tests the `relayERC20` function reverts when the `crossDomainMessageSender` that sent the message is not + /// the same SuperchainERC20 address. + function testFuzz_relayERC20_notCrossDomainSender_reverts( + address _crossDomainMessageSender, + address _to, + uint256 _amount + ) + public + { + vm.assume(_to != ZERO_ADDRESS); + vm.assume(_crossDomainMessageSender != address(superchainERC20)); + + // Mock the call over the `crossDomainMessageSender` function setting a wrong sender + vm.mockCall( + MESSENGER, + abi.encodeWithSelector(IL2ToL2CrossDomainMessenger.crossDomainMessageSender.selector), + abi.encode(_crossDomainMessageSender) + ); + + // Expect the revert with `MessageSenderNotThisSuperchainERC20` selector + vm.expectRevert(MessageSenderNotThisSuperchainERC20.selector); + + // Call the `relayERC20` function with the sender caller + vm.prank(MESSENGER); + superchainERC20.relayERC20(_to, _amount); + } + + /// @notice Tests the `relayERC20` function reverts when the `_to` address is the zero address. + function testFuzz_relayERC20_zeroAddressTo_reverts(uint256 _amount) public { + // Expect the revert with `ZeroAddress` selector + vm.expectRevert(ZeroAddress.selector); + + // Mock the call over the `crossDomainMessageSender` function setting the same address as value + vm.mockCall( + MESSENGER, + abi.encodeWithSelector(IL2ToL2CrossDomainMessenger.crossDomainMessageSender.selector), + abi.encode(address(superchainERC20)) + ); + + // Call the `relayERC20` function with the zero address + vm.prank(MESSENGER); + superchainERC20.relayERC20({ _to: ZERO_ADDRESS, _amount: _amount }); + } + + /// @notice Tests the `relayERC20` mints the proper amount and emits the `RelayedERC20` event. + function testFuzz_relayERC20_succeeds(address _to, uint256 _amount) public { + vm.assume(_to != ZERO_ADDRESS); + + // Mock the call over the `crossDomainMessageSender` function setting the same address as value + _mockAndExpect( + MESSENGER, + abi.encodeWithSelector(IL2ToL2CrossDomainMessenger.crossDomainMessageSender.selector), + abi.encode(address(superchainERC20)) + ); + + // Get the total supply and balance of `_to` before the relay to compare later on the assertions + uint256 _totalSupplyBefore = superchainERC20.totalSupply(); + uint256 _toBalanceBefore = superchainERC20.balanceOf(_to); + + // Look for the emit of the `Transfer` event + vm.expectEmit(true, true, true, true, address(superchainERC20)); + emit IERC20.Transfer(ZERO_ADDRESS, _to, _amount); + + // Look for the emit of the `RelayedERC20` event + vm.expectEmit(true, true, true, true, address(superchainERC20)); + emit OptimismSuperchainERC20.RelayedERC20(_to, _amount); + + // Call the `relayERC20` function with the messenger caller + vm.prank(MESSENGER); + superchainERC20.relayERC20(_to, _amount); + + // Check the total supply and balance of `_to` after the relay were updated correctly + assertEq(superchainERC20.totalSupply(), _totalSupplyBefore + _amount); + assertEq(superchainERC20.balanceOf(_to), _toBalanceBefore + _amount); + } + + /// @notice Tests the `decimals` function always returns the correct value. + function testFuzz_decimals_succeeds(uint8 _decimals) public { + OptimismSuperchainERC20 _newSuperchainERC20 = new OptimismSuperchainERC20(REMOTE_TOKEN, NAME, SYMBOL, _decimals); + assertEq(_newSuperchainERC20.decimals(), _decimals); + } + + /// @notice Tests the `REMOTE_TOKEN` function always returns the correct value. + function testFuzz_remoteToken_succeeds(address _remoteToken) public { + OptimismSuperchainERC20 _newSuperchainERC20 = new OptimismSuperchainERC20(_remoteToken, NAME, SYMBOL, DECIMALS); + assertEq(_newSuperchainERC20.REMOTE_TOKEN(), _remoteToken); + } + + /// @notice Tests the `name` function always returns the correct value. + function testFuzz_name_succeeds(string memory _name) public { + OptimismSuperchainERC20 _newSuperchainERC20 = new OptimismSuperchainERC20(REMOTE_TOKEN, _name, SYMBOL, DECIMALS); + assertEq(_newSuperchainERC20.name(), _name); + } + + /// @notice Tests the `symbol` function always returns the correct value. + function testFuzz_symbol_succeeds(string memory _symbol) public { + OptimismSuperchainERC20 _newSuperchainERC20 = new OptimismSuperchainERC20(REMOTE_TOKEN, NAME, _symbol, DECIMALS); + assertEq(_newSuperchainERC20.symbol(), _symbol); + } +} From bb247fa23c3442a88895651efadd944d04765c42 Mon Sep 17 00:00:00 2001 From: agusduha Date: Thu, 25 Jul 2024 19:04:39 -0300 Subject: [PATCH 02/18] fix: contract fixes --- .../src/L2/IOptimismSuperchainERC20.sol | 51 +++++++++++++++ .../src/L2/OptimismSuperchainERC20.sol | 62 +++++++------------ .../test/L2/OptimismSuperchainERC20.t.sol | 52 ++++++++-------- 3 files changed, 100 insertions(+), 65 deletions(-) create mode 100644 packages/contracts-bedrock/src/L2/IOptimismSuperchainERC20.sol diff --git a/packages/contracts-bedrock/src/L2/IOptimismSuperchainERC20.sol b/packages/contracts-bedrock/src/L2/IOptimismSuperchainERC20.sol new file mode 100644 index 000000000000..1699eed15194 --- /dev/null +++ b/packages/contracts-bedrock/src/L2/IOptimismSuperchainERC20.sol @@ -0,0 +1,51 @@ +// SPDX-License-Identifier: MIT +pragma solidity 0.8.25; + +/// @title IOptimismSuperchainERC20 +/// @notice This interface is available on the OptimismSuperchainERC20 contract. +/// We declare it as a separate interface so that it can be used in +/// custom implementations of SuperchainERC20. +interface IOptimismSuperchainERC20 { + /// @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 Emitted whenever tokens are sent to another chain. + /// @param from Address of the sender. + /// @param to Address of the recipient. + /// @param amount Amount of tokens sent. + /// @param chainId Chain ID of the destination chain. + event SentERC20(address indexed from, address indexed to, uint256 amount, uint256 chainId); + + /// @notice Emitted whenever tokens are successfully relayed on this chain. + /// @param to Address of the recipient. + /// @param amount Amount of tokens relayed. + event RelayedERC20(address indexed to, uint256 amount); + + /// @notice Allows the StandardBridge to mint tokens. + /// @param _to Address to mint tokens to. + /// @param _amount Amount of tokens to mint. + function mint(address _to, uint256 _amount) external; + + /// @notice Allows the StandardBridge to burn tokens. + /// @param _from Address to burn tokens from. + /// @param _amount Amount of tokens to burn. + function burn(address _from, uint256 _amount) external; + + /// @notice Sends tokens to some target address on another chain. + /// @param _to Address to send tokens to. + /// @param _amount Amount of tokens to send. + /// @param _chainId Chain ID of the destination chain. + function sendERC20(address _to, uint256 _amount, uint256 _chainId) external; + + /// @notice Relays tokens received from another chain. + /// @param _to Address to relay tokens to. + /// @param _amount Amount of tokens to relay. + function relayERC20(address _to, uint256 _amount) external; +} diff --git a/packages/contracts-bedrock/src/L2/OptimismSuperchainERC20.sol b/packages/contracts-bedrock/src/L2/OptimismSuperchainERC20.sol index 31878c2bf368..1971a325eca7 100644 --- a/packages/contracts-bedrock/src/L2/OptimismSuperchainERC20.sol +++ b/packages/contracts-bedrock/src/L2/OptimismSuperchainERC20.sol @@ -1,7 +1,7 @@ // SPDX-License-Identifier: MIT pragma solidity 0.8.25; -// import { ERC20 } from "@openzeppelin/contracts/token/ERC20/ERC20.sol"; +import { IOptimismSuperchainERC20 } from "src/L2/IOptimismSuperchainERC20.sol"; import { ERC20 } from "@solady/tokens/ERC20.sol"; import { IL2ToL2CrossDomainMessenger } from "src/L2/IL2ToL2CrossDomainMessenger.sol"; import { ISemver } from "src/universal/ISemver.sol"; @@ -10,14 +10,14 @@ import { Predeploys } from "src/libraries/Predeploys.sol"; /// @notice Thrown when attempting to relay a message and the function caller (msg.sender) is not /// L2ToL2CrossDomainMessenger. -error RelayMessageCallerNotL2ToL2CrossDomainMessenger(); +error CallerNotL2ToL2CrossDomainMessenger(); /// @notice Thrown when attempting to relay a message and the cross domain message sender is not this /// OptimismSuperchainERC20. -error MessageSenderNotThisSuperchainERC20(); +error InvalidCrossDomainSender(); /// @notice Thrown when attempting to mint or burn tokens and the function caller is not the StandardBridge. -error CallerNotBridge(); +error OnlyBridge(); /// @notice Thrown when attempting to mint or burn tokens and the account is the zero address. error ZeroAddress(); @@ -27,7 +27,13 @@ error ZeroAddress(); /// @notice OptimismSuperchainERC20 is a standard extension of the base ERC20 token contract that unifies ERC20 token /// bridging to make it fungible across the Superchain. This construction builds on top of the /// L2ToL2CrossDomainMessenger for both replay protection and domain binding. -contract OptimismSuperchainERC20 is ERC20, ISemver { +contract OptimismSuperchainERC20 is IOptimismSuperchainERC20, ERC20, ISemver { + /// @notice Address of the L2ToL2CrossDomainMessenger Predeploy. + address internal constant MESSENGER = Predeploys.L2_TO_L2_CROSS_DOMAIN_MESSENGER; + + /// @notice Address of the StandardBridge Predeploy. + address internal constant BRIDGE = Predeploys.L2_STANDARD_BRIDGE; + /// @notice Address of the corresponding version of this token on the remote chain. address public immutable REMOTE_TOKEN; @@ -40,31 +46,9 @@ contract OptimismSuperchainERC20 is ERC20, ISemver { /// @notice Symbol of the token string private _symbol; - /// @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 Emitted whenever tokens are sent to another chain. - /// @param from Address of the sender. - /// @param to Address of the recipient. - /// @param amount Amount of tokens sent. - /// @param chainId Chain ID of the destination chain. - event SentERC20(address indexed from, address indexed to, uint256 amount, uint256 chainId); - - /// @notice Emitted whenever tokens are successfully relayed on this chain. - /// @param to Address of the recipient. - /// @param amount Amount of tokens relayed. - event RelayedERC20(address indexed to, uint256 amount); - /// @notice A modifier that only allows the bridge to call modifier onlyBridge() { - if (msg.sender != Predeploys.L2_STANDARD_BRIDGE) revert CallerNotBridge(); + if (msg.sender != BRIDGE) revert OnlyBridge(); _; } @@ -115,9 +99,7 @@ contract OptimismSuperchainERC20 is ERC20, ISemver { _burn(msg.sender, _amount); bytes memory _message = abi.encodeCall(this.relayERC20, (_to, _amount)); - IL2ToL2CrossDomainMessenger(Predeploys.L2_TO_L2_CROSS_DOMAIN_MESSENGER).sendMessage( - _chainId, address(this), _message - ); + IL2ToL2CrossDomainMessenger(MESSENGER).sendMessage(_chainId, address(this), _message); emit SentERC20(msg.sender, _to, _amount, _chainId); } @@ -128,15 +110,10 @@ contract OptimismSuperchainERC20 is ERC20, ISemver { function relayERC20(address _to, uint256 _amount) external { if (_to == address(0)) revert ZeroAddress(); - if (msg.sender != Predeploys.L2_TO_L2_CROSS_DOMAIN_MESSENGER) { - revert RelayMessageCallerNotL2ToL2CrossDomainMessenger(); - } + if (msg.sender != MESSENGER) revert CallerNotL2ToL2CrossDomainMessenger(); - if ( - IL2ToL2CrossDomainMessenger(Predeploys.L2_TO_L2_CROSS_DOMAIN_MESSENGER).crossDomainMessageSender() - != address(this) - ) { - revert MessageSenderNotThisSuperchainERC20(); + if (IL2ToL2CrossDomainMessenger(MESSENGER).crossDomainMessageSender() != address(this)) { + revert InvalidCrossDomainSender(); } _mint(_to, _amount); @@ -163,4 +140,11 @@ contract OptimismSuperchainERC20 is ERC20, ISemver { function symbol() public view virtual override returns (string memory) { return _symbol; } + + /// @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) { + return _interfaceId == type(IOptimismSuperchainERC20).interfaceId; + } } diff --git a/packages/contracts-bedrock/test/L2/OptimismSuperchainERC20.t.sol b/packages/contracts-bedrock/test/L2/OptimismSuperchainERC20.t.sol index 76e47e70d9f1..6f9e6bb9c676 100644 --- a/packages/contracts-bedrock/test/L2/OptimismSuperchainERC20.t.sol +++ b/packages/contracts-bedrock/test/L2/OptimismSuperchainERC20.t.sol @@ -14,10 +14,10 @@ import { IL2ToL2CrossDomainMessenger } from "src/L2/IL2ToL2CrossDomainMessenger. // Target contract import { OptimismSuperchainERC20, - CallerNotBridge, - RelayMessageCallerNotL2ToL2CrossDomainMessenger, - MessageSenderNotThisSuperchainERC20, - CallerNotBridge, + IOptimismSuperchainERC20, + CallerNotL2ToL2CrossDomainMessenger, + InvalidCrossDomainSender, + OnlyBridge, ZeroAddress } from "src/L2/OptimismSuperchainERC20.sol"; @@ -58,8 +58,8 @@ contract OptimismSuperchainERC20Test is Test { // Ensure the caller is not the bridge vm.assume(_caller != BRIDGE); - // Expect the revert with `CallerNotBridge` selector - vm.expectRevert(CallerNotBridge.selector); + // Expect the revert with `OnlyBridge` selector + vm.expectRevert(OnlyBridge.selector); // Call the `mint` function with the non-bridge caller vm.prank(_caller); @@ -91,7 +91,7 @@ contract OptimismSuperchainERC20Test is Test { // Look for the emit of the `Mint` event vm.expectEmit(true, true, true, true, address(superchainERC20)); - emit OptimismSuperchainERC20.Mint(_to, _amount); + emit IOptimismSuperchainERC20.Mint(_to, _amount); // Call the `mint` function with the bridge caller vm.prank(BRIDGE); @@ -107,8 +107,8 @@ contract OptimismSuperchainERC20Test is Test { // Ensure the caller is not the bridge vm.assume(_caller != BRIDGE); - // Expect the revert with `CallerNotBridge` selector - vm.expectRevert(CallerNotBridge.selector); + // Expect the revert with `OnlyBridge` selector + vm.expectRevert(OnlyBridge.selector); // Call the `burn` function with the non-bridge caller vm.prank(_caller); @@ -144,7 +144,7 @@ contract OptimismSuperchainERC20Test is Test { // Look for the emit of the `Burn` event vm.expectEmit(true, true, true, true, address(superchainERC20)); - emit OptimismSuperchainERC20.Burn(_from, _amount); + emit IOptimismSuperchainERC20.Burn(_from, _amount); // Call the `burn` function with the bridge caller vm.prank(BRIDGE); @@ -155,6 +155,16 @@ contract OptimismSuperchainERC20Test is Test { assertEq(superchainERC20.balanceOf(_from), _fromBalanceBefore - _amount); } + /// @notice Tests the `sendERC20` function reverts when the `_to` address is the zero address. + function testFuzz_sendERC20_zeroAddressTo_reverts(uint256 _amount, uint256 _chainId) public { + // Expect the revert with `ZeroAddress` selector + vm.expectRevert(ZeroAddress.selector); + + // Call the `sendERC20` function with the zero address + vm.prank(BRIDGE); + superchainERC20.sendERC20({ _to: ZERO_ADDRESS, _amount: _amount, _chainId: _chainId }); + } + /// @notice Tests the `sendERC20` function burns the sender tokens, sends the message, and emits the `SentERC20` /// event. function testFuzz_sendERC20_succeeds(address _sender, address _to, uint256 _amount, uint256 _chainId) external { @@ -176,7 +186,7 @@ contract OptimismSuperchainERC20Test is Test { // Look for the emit of the `SentERC20` event vm.expectEmit(true, true, true, true, address(superchainERC20)); - emit OptimismSuperchainERC20.SentERC20(_sender, _to, _amount, _chainId); + emit IOptimismSuperchainERC20.SentERC20(_sender, _to, _amount, _chainId); // Mock the call over the `sendMessage` function and expect it to be called properly bytes memory _message = abi.encodeCall(superchainERC20.relayERC20, (_to, _amount)); @@ -197,24 +207,14 @@ contract OptimismSuperchainERC20Test is Test { assertEq(superchainERC20.balanceOf(_sender), _senderBalanceBefore - _amount); } - /// @notice Tests the `sendERC20` function reverts when the `_to` address is the zero address. - function testFuzz_sendERC20_zeroAddressTo_reverts(uint256 _amount, uint256 _chainId) public { - // Expect the revert with `ZeroAddress` selector - vm.expectRevert(ZeroAddress.selector); - - // Call the `sendERC20` function with the zero address - vm.prank(BRIDGE); - superchainERC20.sendERC20({ _to: ZERO_ADDRESS, _amount: _amount, _chainId: _chainId }); - } - /// @notice Tests the `relayERC20` function reverts when the caller is not the L2ToL2CrossDomainMessenger. function testFuzz_relayERC20_notMessenger_reverts(address _caller, address _to, uint256 _amount) public { // Ensure the caller is not the messenger vm.assume(_caller != MESSENGER); vm.assume(_to != ZERO_ADDRESS); - // Expect the revert with `RelayMessageCallerNotL2ToL2CrossDomainMessenger` selector - vm.expectRevert(RelayMessageCallerNotL2ToL2CrossDomainMessenger.selector); + // Expect the revert with `CallerNotL2ToL2CrossDomainMessenger` selector + vm.expectRevert(CallerNotL2ToL2CrossDomainMessenger.selector); // Call the `relayERC20` function with the non-messenger caller vm.prank(_caller); @@ -240,8 +240,8 @@ contract OptimismSuperchainERC20Test is Test { abi.encode(_crossDomainMessageSender) ); - // Expect the revert with `MessageSenderNotThisSuperchainERC20` selector - vm.expectRevert(MessageSenderNotThisSuperchainERC20.selector); + // Expect the revert with `InvalidCrossDomainSender` selector + vm.expectRevert(InvalidCrossDomainSender.selector); // Call the `relayERC20` function with the sender caller vm.prank(MESSENGER); @@ -286,7 +286,7 @@ contract OptimismSuperchainERC20Test is Test { // Look for the emit of the `RelayedERC20` event vm.expectEmit(true, true, true, true, address(superchainERC20)); - emit OptimismSuperchainERC20.RelayedERC20(_to, _amount); + emit IOptimismSuperchainERC20.RelayedERC20(_to, _amount); // Call the `relayERC20` function with the messenger caller vm.prank(MESSENGER); From feea60d4c523495be3d6bcabb3f0b3d0671fabc0 Mon Sep 17 00:00:00 2001 From: agusduha Date: Thu, 25 Jul 2024 19:05:12 -0300 Subject: [PATCH 03/18] feat: add snapshots and semver --- packages/contracts-bedrock/semver-lock.json | 4 + .../abi/OptimismSuperchainERC20.json | 590 ++++++++++++++++++ .../OptimismSuperchainERC20.json | 16 + 3 files changed, 610 insertions(+) create mode 100644 packages/contracts-bedrock/snapshots/abi/OptimismSuperchainERC20.json create mode 100644 packages/contracts-bedrock/snapshots/storageLayout/OptimismSuperchainERC20.json diff --git a/packages/contracts-bedrock/semver-lock.json b/packages/contracts-bedrock/semver-lock.json index 292eb473840a..524f3b0fea6f 100644 --- a/packages/contracts-bedrock/semver-lock.json +++ b/packages/contracts-bedrock/semver-lock.json @@ -103,6 +103,10 @@ "initCodeHash": "0x15fbb6175eb98a7d7c6b99862de49e8c3f8ac768c656e82ad7c41c0d1739bd66", "sourceCodeHash": "0x1f14aafab2cb15970cccedb461b72218fca8afa6ffd0ac696a9e28ff1415a068" }, + "src/L2/OptimismSuperchainERC20.sol": { + "initCodeHash": "0xd7418787c42d9bfdc85f7810deb0f65a014edace04a04f81af218940f0c32a54", + "sourceCodeHash": "0x758e963a4c873e1698b5c5fbb69834b505e29b8aaf90c359210e806a86182c7b" + }, "src/L2/SequencerFeeVault.sol": { "initCodeHash": "0xb94145f571e92ee615c6fe903b6568e8aac5fe760b6b65148ffc45d2fb0f5433", "sourceCodeHash": "0x8f2a54104e5e7105ba03ba37e3ef9b6684a447245f0e0b787ba4cca12957b97c" diff --git a/packages/contracts-bedrock/snapshots/abi/OptimismSuperchainERC20.json b/packages/contracts-bedrock/snapshots/abi/OptimismSuperchainERC20.json new file mode 100644 index 000000000000..eb1fd4b750ec --- /dev/null +++ b/packages/contracts-bedrock/snapshots/abi/OptimismSuperchainERC20.json @@ -0,0 +1,590 @@ +[ + { + "inputs": [ + { + "internalType": "address", + "name": "_remoteToken", + "type": "address" + }, + { + "internalType": "string", + "name": "_tokenName", + "type": "string" + }, + { + "internalType": "string", + "name": "_tokenSymbol", + "type": "string" + }, + { + "internalType": "uint8", + "name": "_decimals", + "type": "uint8" + } + ], + "stateMutability": "nonpayable", + "type": "constructor" + }, + { + "inputs": [], + "name": "DOMAIN_SEPARATOR", + "outputs": [ + { + "internalType": "bytes32", + "name": "result", + "type": "bytes32" + } + ], + "stateMutability": "view", + "type": "function" + }, + { + "inputs": [], + "name": "REMOTE_TOKEN", + "outputs": [ + { + "internalType": "address", + "name": "", + "type": "address" + } + ], + "stateMutability": "view", + "type": "function" + }, + { + "inputs": [ + { + "internalType": "address", + "name": "owner", + "type": "address" + }, + { + "internalType": "address", + "name": "spender", + "type": "address" + } + ], + "name": "allowance", + "outputs": [ + { + "internalType": "uint256", + "name": "result", + "type": "uint256" + } + ], + "stateMutability": "view", + "type": "function" + }, + { + "inputs": [ + { + "internalType": "address", + "name": "spender", + "type": "address" + }, + { + "internalType": "uint256", + "name": "amount", + "type": "uint256" + } + ], + "name": "approve", + "outputs": [ + { + "internalType": "bool", + "name": "", + "type": "bool" + } + ], + "stateMutability": "nonpayable", + "type": "function" + }, + { + "inputs": [ + { + "internalType": "address", + "name": "owner", + "type": "address" + } + ], + "name": "balanceOf", + "outputs": [ + { + "internalType": "uint256", + "name": "result", + "type": "uint256" + } + ], + "stateMutability": "view", + "type": "function" + }, + { + "inputs": [ + { + "internalType": "address", + "name": "_from", + "type": "address" + }, + { + "internalType": "uint256", + "name": "_amount", + "type": "uint256" + } + ], + "name": "burn", + "outputs": [], + "stateMutability": "nonpayable", + "type": "function" + }, + { + "inputs": [], + "name": "decimals", + "outputs": [ + { + "internalType": "uint8", + "name": "", + "type": "uint8" + } + ], + "stateMutability": "view", + "type": "function" + }, + { + "inputs": [ + { + "internalType": "address", + "name": "_to", + "type": "address" + }, + { + "internalType": "uint256", + "name": "_amount", + "type": "uint256" + } + ], + "name": "mint", + "outputs": [], + "stateMutability": "nonpayable", + "type": "function" + }, + { + "inputs": [], + "name": "name", + "outputs": [ + { + "internalType": "string", + "name": "", + "type": "string" + } + ], + "stateMutability": "view", + "type": "function" + }, + { + "inputs": [ + { + "internalType": "address", + "name": "owner", + "type": "address" + } + ], + "name": "nonces", + "outputs": [ + { + "internalType": "uint256", + "name": "result", + "type": "uint256" + } + ], + "stateMutability": "view", + "type": "function" + }, + { + "inputs": [ + { + "internalType": "address", + "name": "owner", + "type": "address" + }, + { + "internalType": "address", + "name": "spender", + "type": "address" + }, + { + "internalType": "uint256", + "name": "value", + "type": "uint256" + }, + { + "internalType": "uint256", + "name": "deadline", + "type": "uint256" + }, + { + "internalType": "uint8", + "name": "v", + "type": "uint8" + }, + { + "internalType": "bytes32", + "name": "r", + "type": "bytes32" + }, + { + "internalType": "bytes32", + "name": "s", + "type": "bytes32" + } + ], + "name": "permit", + "outputs": [], + "stateMutability": "nonpayable", + "type": "function" + }, + { + "inputs": [ + { + "internalType": "address", + "name": "_to", + "type": "address" + }, + { + "internalType": "uint256", + "name": "_amount", + "type": "uint256" + } + ], + "name": "relayERC20", + "outputs": [], + "stateMutability": "nonpayable", + "type": "function" + }, + { + "inputs": [ + { + "internalType": "address", + "name": "_to", + "type": "address" + }, + { + "internalType": "uint256", + "name": "_amount", + "type": "uint256" + }, + { + "internalType": "uint256", + "name": "_chainId", + "type": "uint256" + } + ], + "name": "sendERC20", + "outputs": [], + "stateMutability": "nonpayable", + "type": "function" + }, + { + "inputs": [ + { + "internalType": "bytes4", + "name": "_interfaceId", + "type": "bytes4" + } + ], + "name": "supportsInterface", + "outputs": [ + { + "internalType": "bool", + "name": "", + "type": "bool" + } + ], + "stateMutability": "pure", + "type": "function" + }, + { + "inputs": [], + "name": "symbol", + "outputs": [ + { + "internalType": "string", + "name": "", + "type": "string" + } + ], + "stateMutability": "view", + "type": "function" + }, + { + "inputs": [], + "name": "totalSupply", + "outputs": [ + { + "internalType": "uint256", + "name": "result", + "type": "uint256" + } + ], + "stateMutability": "view", + "type": "function" + }, + { + "inputs": [ + { + "internalType": "address", + "name": "to", + "type": "address" + }, + { + "internalType": "uint256", + "name": "amount", + "type": "uint256" + } + ], + "name": "transfer", + "outputs": [ + { + "internalType": "bool", + "name": "", + "type": "bool" + } + ], + "stateMutability": "nonpayable", + "type": "function" + }, + { + "inputs": [ + { + "internalType": "address", + "name": "from", + "type": "address" + }, + { + "internalType": "address", + "name": "to", + "type": "address" + }, + { + "internalType": "uint256", + "name": "amount", + "type": "uint256" + } + ], + "name": "transferFrom", + "outputs": [ + { + "internalType": "bool", + "name": "", + "type": "bool" + } + ], + "stateMutability": "nonpayable", + "type": "function" + }, + { + "inputs": [], + "name": "version", + "outputs": [ + { + "internalType": "string", + "name": "", + "type": "string" + } + ], + "stateMutability": "view", + "type": "function" + }, + { + "anonymous": false, + "inputs": [ + { + "indexed": true, + "internalType": "address", + "name": "owner", + "type": "address" + }, + { + "indexed": true, + "internalType": "address", + "name": "spender", + "type": "address" + }, + { + "indexed": false, + "internalType": "uint256", + "name": "amount", + "type": "uint256" + } + ], + "name": "Approval", + "type": "event" + }, + { + "anonymous": false, + "inputs": [ + { + "indexed": true, + "internalType": "address", + "name": "account", + "type": "address" + }, + { + "indexed": false, + "internalType": "uint256", + "name": "amount", + "type": "uint256" + } + ], + "name": "Burn", + "type": "event" + }, + { + "anonymous": false, + "inputs": [ + { + "indexed": true, + "internalType": "address", + "name": "account", + "type": "address" + }, + { + "indexed": false, + "internalType": "uint256", + "name": "amount", + "type": "uint256" + } + ], + "name": "Mint", + "type": "event" + }, + { + "anonymous": false, + "inputs": [ + { + "indexed": true, + "internalType": "address", + "name": "to", + "type": "address" + }, + { + "indexed": false, + "internalType": "uint256", + "name": "amount", + "type": "uint256" + } + ], + "name": "RelayedERC20", + "type": "event" + }, + { + "anonymous": false, + "inputs": [ + { + "indexed": true, + "internalType": "address", + "name": "from", + "type": "address" + }, + { + "indexed": true, + "internalType": "address", + "name": "to", + "type": "address" + }, + { + "indexed": false, + "internalType": "uint256", + "name": "amount", + "type": "uint256" + }, + { + "indexed": false, + "internalType": "uint256", + "name": "chainId", + "type": "uint256" + } + ], + "name": "SentERC20", + "type": "event" + }, + { + "anonymous": false, + "inputs": [ + { + "indexed": true, + "internalType": "address", + "name": "from", + "type": "address" + }, + { + "indexed": true, + "internalType": "address", + "name": "to", + "type": "address" + }, + { + "indexed": false, + "internalType": "uint256", + "name": "amount", + "type": "uint256" + } + ], + "name": "Transfer", + "type": "event" + }, + { + "inputs": [], + "name": "AllowanceOverflow", + "type": "error" + }, + { + "inputs": [], + "name": "AllowanceUnderflow", + "type": "error" + }, + { + "inputs": [], + "name": "CallerNotL2ToL2CrossDomainMessenger", + "type": "error" + }, + { + "inputs": [], + "name": "InsufficientAllowance", + "type": "error" + }, + { + "inputs": [], + "name": "InsufficientBalance", + "type": "error" + }, + { + "inputs": [], + "name": "InvalidCrossDomainSender", + "type": "error" + }, + { + "inputs": [], + "name": "InvalidPermit", + "type": "error" + }, + { + "inputs": [], + "name": "OnlyBridge", + "type": "error" + }, + { + "inputs": [], + "name": "PermitExpired", + "type": "error" + }, + { + "inputs": [], + "name": "TotalSupplyOverflow", + "type": "error" + }, + { + "inputs": [], + "name": "ZeroAddress", + "type": "error" + } +] \ No newline at end of file diff --git a/packages/contracts-bedrock/snapshots/storageLayout/OptimismSuperchainERC20.json b/packages/contracts-bedrock/snapshots/storageLayout/OptimismSuperchainERC20.json new file mode 100644 index 000000000000..76bff10681c2 --- /dev/null +++ b/packages/contracts-bedrock/snapshots/storageLayout/OptimismSuperchainERC20.json @@ -0,0 +1,16 @@ +[ + { + "bytes": "32", + "label": "_name", + "offset": 0, + "slot": "0", + "type": "string" + }, + { + "bytes": "32", + "label": "_symbol", + "offset": 0, + "slot": "1", + "type": "string" + } +] \ No newline at end of file From c23e226958bd923aa0cdfece9fec3468a1ba603b Mon Sep 17 00:00:00 2001 From: agusduha Date: Fri, 26 Jul 2024 11:38:09 -0300 Subject: [PATCH 04/18] test: add supports interface tests --- .../src/L2/OptimismSuperchainERC20.sol | 4 ++-- .../test/L2/OptimismSuperchainERC20.t.sol | 12 ++++++++++++ 2 files changed, 14 insertions(+), 2 deletions(-) diff --git a/packages/contracts-bedrock/src/L2/OptimismSuperchainERC20.sol b/packages/contracts-bedrock/src/L2/OptimismSuperchainERC20.sol index 1971a325eca7..3cca0cdbd96d 100644 --- a/packages/contracts-bedrock/src/L2/OptimismSuperchainERC20.sol +++ b/packages/contracts-bedrock/src/L2/OptimismSuperchainERC20.sol @@ -67,7 +67,7 @@ contract OptimismSuperchainERC20 is IOptimismSuperchainERC20, ERC20, ISemver { _symbol = _tokenSymbol; } - /// @notice Allows the StandardBridge to mint tokens. + /// @notice Allows the L2StandardBridge 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 onlyBridge { @@ -78,7 +78,7 @@ contract OptimismSuperchainERC20 is IOptimismSuperchainERC20, ERC20, ISemver { emit Mint(_to, _amount); } - /// @notice Allows the StandardBridge to burn tokens. + /// @notice Allows the L2StandardBridge 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 onlyBridge { diff --git a/packages/contracts-bedrock/test/L2/OptimismSuperchainERC20.t.sol b/packages/contracts-bedrock/test/L2/OptimismSuperchainERC20.t.sol index 6f9e6bb9c676..ee31b9f87bc7 100644 --- a/packages/contracts-bedrock/test/L2/OptimismSuperchainERC20.t.sol +++ b/packages/contracts-bedrock/test/L2/OptimismSuperchainERC20.t.sol @@ -320,4 +320,16 @@ contract OptimismSuperchainERC20Test is Test { OptimismSuperchainERC20 _newSuperchainERC20 = new OptimismSuperchainERC20(REMOTE_TOKEN, NAME, _symbol, DECIMALS); assertEq(_newSuperchainERC20.symbol(), _symbol); } + + /// @notice Tests that the `supportsInterface` function returns true for the `IOptimismSuperchainERC20` interface. + function test_supportInterface_succeeds() public view { + assertTrue(superchainERC20.supportsInterface(type(IOptimismSuperchainERC20).interfaceId)); + } + + /// @notice Tests that the `supportsInterface` function returns false for any other interface than the + /// `IOptimismSuperchainERC20` one. + function testFuzz_supportInterface_returnFalse(bytes4 _interfaceId) public view { + vm.assume(_interfaceId != type(IOptimismSuperchainERC20).interfaceId); + assertFalse(superchainERC20.supportsInterface(_interfaceId)); + } } From 61d10d9a5c3960067a18e7c4dcbe8fc33e75c684 Mon Sep 17 00:00:00 2001 From: agusduha Date: Fri, 26 Jul 2024 16:44:54 -0300 Subject: [PATCH 05/18] test: add invariant test --- .../invariant-docs/OptimismSuperchainERC20.md | 5 + .../invariant-docs/README.md | 1 + packages/contracts-bedrock/semver-lock.json | 2 +- .../src/L2/IOptimismSuperchainERC20.sol | 4 +- .../src/L2/OptimismSuperchainERC20.sol | 8 +- .../invariants/OptimismSuperchainERC20.t.sol | 169 ++++++++++++++++++ 6 files changed, 183 insertions(+), 6 deletions(-) create mode 100644 packages/contracts-bedrock/invariant-docs/OptimismSuperchainERC20.md create mode 100644 packages/contracts-bedrock/test/invariants/OptimismSuperchainERC20.t.sol diff --git a/packages/contracts-bedrock/invariant-docs/OptimismSuperchainERC20.md b/packages/contracts-bedrock/invariant-docs/OptimismSuperchainERC20.md new file mode 100644 index 000000000000..5bdbd57a4d19 --- /dev/null +++ b/packages/contracts-bedrock/invariant-docs/OptimismSuperchainERC20.md @@ -0,0 +1,5 @@ +# `OptimismSuperchainERC20` Invariants + +## Calls to sendERC20 should always succeed as long as the actor has enough balance. Actor's balance should also not increase out of nowhere. +**Test:** [`OptimismSuperchainERC20.t.sol#L150`](../test/invariants/OptimismSuperchainERC20.t.sol#L150) + diff --git a/packages/contracts-bedrock/invariant-docs/README.md b/packages/contracts-bedrock/invariant-docs/README.md index eae292a89cc2..6b327804569c 100644 --- a/packages/contracts-bedrock/invariant-docs/README.md +++ b/packages/contracts-bedrock/invariant-docs/README.md @@ -17,6 +17,7 @@ This directory contains documentation for all defined invariant tests within `co - [L2OutputOracle](./L2OutputOracle.md) - [OptimismPortal](./OptimismPortal.md) - [OptimismPortal2](./OptimismPortal2.md) +- [OptimismSuperchainERC20](./OptimismSuperchainERC20.md) - [ResourceMetering](./ResourceMetering.md) - [SafeCall](./SafeCall.md) - [SystemConfig](./SystemConfig.md) diff --git a/packages/contracts-bedrock/semver-lock.json b/packages/contracts-bedrock/semver-lock.json index 524f3b0fea6f..b79ab06d6700 100644 --- a/packages/contracts-bedrock/semver-lock.json +++ b/packages/contracts-bedrock/semver-lock.json @@ -105,7 +105,7 @@ }, "src/L2/OptimismSuperchainERC20.sol": { "initCodeHash": "0xd7418787c42d9bfdc85f7810deb0f65a014edace04a04f81af218940f0c32a54", - "sourceCodeHash": "0x758e963a4c873e1698b5c5fbb69834b505e29b8aaf90c359210e806a86182c7b" + "sourceCodeHash": "0xfd07d6685da601129bd854f79e91363baf9d93d28868add9ca7a0698111158a1" }, "src/L2/SequencerFeeVault.sol": { "initCodeHash": "0xb94145f571e92ee615c6fe903b6568e8aac5fe760b6b65148ffc45d2fb0f5433", diff --git a/packages/contracts-bedrock/src/L2/IOptimismSuperchainERC20.sol b/packages/contracts-bedrock/src/L2/IOptimismSuperchainERC20.sol index 1699eed15194..3c6ff28dbb93 100644 --- a/packages/contracts-bedrock/src/L2/IOptimismSuperchainERC20.sol +++ b/packages/contracts-bedrock/src/L2/IOptimismSuperchainERC20.sol @@ -28,12 +28,12 @@ interface IOptimismSuperchainERC20 { /// @param amount Amount of tokens relayed. event RelayedERC20(address indexed to, uint256 amount); - /// @notice Allows the StandardBridge to mint tokens. + /// @notice Allows the L2StandardBridge to mint tokens. /// @param _to Address to mint tokens to. /// @param _amount Amount of tokens to mint. function mint(address _to, uint256 _amount) external; - /// @notice Allows the StandardBridge to burn tokens. + /// @notice Allows the L2StandardBridge to burn tokens. /// @param _from Address to burn tokens from. /// @param _amount Amount of tokens to burn. function burn(address _from, uint256 _amount) external; diff --git a/packages/contracts-bedrock/src/L2/OptimismSuperchainERC20.sol b/packages/contracts-bedrock/src/L2/OptimismSuperchainERC20.sol index 3cca0cdbd96d..b224703316c5 100644 --- a/packages/contracts-bedrock/src/L2/OptimismSuperchainERC20.sol +++ b/packages/contracts-bedrock/src/L2/OptimismSuperchainERC20.sol @@ -5,7 +5,6 @@ import { IOptimismSuperchainERC20 } from "src/L2/IOptimismSuperchainERC20.sol"; import { ERC20 } from "@solady/tokens/ERC20.sol"; import { IL2ToL2CrossDomainMessenger } from "src/L2/IL2ToL2CrossDomainMessenger.sol"; import { ISemver } from "src/universal/ISemver.sol"; -import { SafeCall } from "src/libraries/SafeCall.sol"; import { Predeploys } from "src/libraries/Predeploys.sol"; /// @notice Thrown when attempting to relay a message and the function caller (msg.sender) is not @@ -25,8 +24,11 @@ error ZeroAddress(); /// @custom:proxied /// @title OptimismSuperchainERC20 /// @notice OptimismSuperchainERC20 is a standard extension of the base ERC20 token contract that unifies ERC20 token -/// bridging to make it fungible across the Superchain. This construction builds on top of the -/// L2ToL2CrossDomainMessenger for both replay protection and domain binding. +/// bridging to make it fungible across the Superchain. This construction allows the L2StandardBridge to burn +/// and mint tokens. This makes it possible to convert a valid OptimismMintableERC20 token to a SuperchainERC20 +/// 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 IOptimismSuperchainERC20, ERC20, ISemver { /// @notice Address of the L2ToL2CrossDomainMessenger Predeploy. address internal constant MESSENGER = Predeploys.L2_TO_L2_CROSS_DOMAIN_MESSENGER; diff --git a/packages/contracts-bedrock/test/invariants/OptimismSuperchainERC20.t.sol b/packages/contracts-bedrock/test/invariants/OptimismSuperchainERC20.t.sol new file mode 100644 index 000000000000..17243db3e7a4 --- /dev/null +++ b/packages/contracts-bedrock/test/invariants/OptimismSuperchainERC20.t.sol @@ -0,0 +1,169 @@ +// SPDX-License-Identifier: MIT +pragma solidity 0.8.25; + +// Testing utilities +import { Test, StdUtils, Vm } from "forge-std/Test.sol"; +import { EIP1967Helper } from "test/mocks/EIP1967Helper.sol"; + +// Libraries +import { Predeploys } from "src/libraries/Predeploys.sol"; +import { OptimismSuperchainERC20 } from "src/L2/OptimismSuperchainERC20.sol"; +import { IL2ToL2CrossDomainMessenger } from "src/L2/IL2ToL2CrossDomainMessenger.sol"; + +/// @title OptimismSuperchainERC20_User +/// @notice Actor contract that interacts with the OptimismSuperchainERC20 contract. +contract OptimismSuperchainERC20_User is StdUtils { + /// @notice Cross domain message data. + struct MessageData { + bytes32 id; + uint256 amount; + } + + /// @notice Flag to indicate if the test has failed. + bool public failed = false; + + /// @notice The Vm contract. + Vm internal vm; + + /// @notice The OptimismSuperchainERC20 contract. + OptimismSuperchainERC20 internal superchainERC20; + + /// @notice Mapping of sent messages. + mapping(bytes32 => bool) internal sent; + + /// @notice Array of unrelayed messages. + MessageData[] internal unrelayed; + + /// @param _vm The Vm contract. + /// @param _superchainERC20 The OptimismSuperchainERC20 contract. + /// @param _balance The initial balance of the contract. + constructor(Vm _vm, OptimismSuperchainERC20 _superchainERC20, uint256 _balance) { + vm = _vm; + superchainERC20 = _superchainERC20; + + // Mint balance to this actor. + vm.prank(Predeploys.L2_STANDARD_BRIDGE); + superchainERC20.mint(address(this), _balance); + } + + /// @notice Send ERC20 tokens to another chain. + /// @param _amount The amount of ERC20 tokens to send. + /// @param _chainId The chain ID to send the tokens to. + /// @param _messageId The message ID. + function sendERC20(uint256 _amount, uint256 _chainId, bytes32 _messageId) public { + // Make sure we aren't reusing a message ID. + if (sent[_messageId]) { + return; + } + + // Bound send amount to our ERC20 balance. + _amount = bound(_amount, 0, superchainERC20.balanceOf(address(this))); + + // Send the amount. + try superchainERC20.sendERC20(address(this), _amount, _chainId) { + // Success. + } catch { + failed = true; + } + + // Mark message as sent. + sent[_messageId] = true; + unrelayed.push(MessageData({ id: _messageId, amount: _amount })); + } + + /// @notice Relay a message from another chain. + function relayMessage() public { + // Make sure there are unrelayed messages. + if (unrelayed.length == 0) { + return; + } + + // Grab the latest unrelayed message. + MessageData memory message = unrelayed[unrelayed.length - 1]; + + // Simulate the cross-domain message. + // Make sure the cross-domain message sender is set to this contract. + vm.mockCall( + Predeploys.L2_TO_L2_CROSS_DOMAIN_MESSENGER, + abi.encodeCall(IL2ToL2CrossDomainMessenger.crossDomainMessageSender, ()), + abi.encode(address(superchainERC20)) + ); + + // Prank the relayERC20 function. + // Balance will just go back to our own account. + vm.prank(Predeploys.L2_TO_L2_CROSS_DOMAIN_MESSENGER); + try superchainERC20.relayERC20(address(this), message.amount) { + // Success. + } catch { + failed = true; + } + + // Remove the message from the unrelayed list. + unrelayed.pop(); + } +} + +/// @title OptimismSuperchainERC20_SendSucceeds_Invariant +/// @notice Invariant test that checks that sending OptimismSuperchainERC20 always succeeds if the actor has a +/// sufficient balance to do so and that the actor's balance does not increase out of nowhere. +contract OptimismSuperchainERC20_SendSucceeds_Invariant is Test { + /// @notice Starting balance of the contract. + uint256 internal constant STARTING_BALANCE = type(uint256).max; + + /// @notice The OptimismSuperchainERC20_User actor. + OptimismSuperchainERC20_User internal actor; + + /// @notice The OptimismSuperchainERC20 contract. + OptimismSuperchainERC20 internal optimismSuperchainERC20; + + /// @notice Test setup. + function setUp() public { + // Deploy the L2ToL2CrossDomainMessenger contract. + address _impl = _setImplementationCode(Predeploys.L2_TO_L2_CROSS_DOMAIN_MESSENGER); + _setProxyCode(Predeploys.L2_TO_L2_CROSS_DOMAIN_MESSENGER, _impl); + + // Create a new OptimismSuperchainERC20 + optimismSuperchainERC20 = new OptimismSuperchainERC20(address(0x123), "Supertoken", "SUP", 18); + + // Create a new OptimismSuperchainERC20_User actor. + actor = new OptimismSuperchainERC20_User(vm, optimismSuperchainERC20, STARTING_BALANCE); + + // Set the target contract. + targetContract(address(actor)); + + // Set the target selectors. + bytes4[] memory selectors = new bytes4[](2); + selectors[0] = actor.sendERC20.selector; + selectors[1] = actor.relayMessage.selector; + FuzzSelector memory selector = FuzzSelector({ addr: address(actor), selectors: selectors }); + targetSelector(selector); + } + + /// @notice Sets the bytecode in the implementation address. + function _setImplementationCode(address _addr) internal returns (address) { + string memory cname = Predeploys.getName(_addr); + address impl = Predeploys.predeployToCodeNamespace(_addr); + vm.etch(impl, vm.getDeployedCode(string.concat(cname, ".sol:", cname))); + return impl; + } + + /// @notice Sets the bytecode in the proxy address. + function _setProxyCode(address _addr, address _impl) internal { + bytes memory code = vm.getDeployedCode("Proxy.sol:Proxy"); + vm.etch(_addr, code); + EIP1967Helper.setAdmin(_addr, Predeploys.PROXY_ADMIN); + EIP1967Helper.setImplementation(_addr, _impl); + } + + /// @notice Invariant that checks that sending OptimismSuperchainERC20 always succeeds. + /// @custom:invariant Calls to sendERC20 should always succeed as long as the actor has enough balance. + /// Actor's balance should also not increase out of nowhere. + function invariant_sendERC20_succeeds() public view { + // Assert that the actor has not failed to send OptimismSuperchainERC20. + assertEq(actor.failed(), false); + + // Assert that the actor's balance has not somehow increased. + assertLe(optimismSuperchainERC20.balanceOf(address(actor)), STARTING_BALANCE); + assertLe(optimismSuperchainERC20.totalSupply(), STARTING_BALANCE); + } +} From d1d2f2819ba03ec4adb410ac9319a742c61200cb Mon Sep 17 00:00:00 2001 From: agusduha Date: Fri, 26 Jul 2024 17:18:59 -0300 Subject: [PATCH 06/18] feat: add parameters to the RelayERC20 event --- packages/contracts-bedrock/semver-lock.json | 4 +-- .../abi/OptimismSuperchainERC20.json | 19 +++++++++++++- .../src/L2/IOptimismSuperchainERC20.sol | 25 +++++++++++-------- .../src/L2/OptimismSuperchainERC20.sol | 8 +++--- .../test/L2/OptimismSuperchainERC20.t.sol | 22 ++++++++++------ .../invariants/OptimismSuperchainERC20.t.sol | 11 ++++++-- 6 files changed, 63 insertions(+), 26 deletions(-) diff --git a/packages/contracts-bedrock/semver-lock.json b/packages/contracts-bedrock/semver-lock.json index b79ab06d6700..619a4ddb10f1 100644 --- a/packages/contracts-bedrock/semver-lock.json +++ b/packages/contracts-bedrock/semver-lock.json @@ -104,8 +104,8 @@ "sourceCodeHash": "0x1f14aafab2cb15970cccedb461b72218fca8afa6ffd0ac696a9e28ff1415a068" }, "src/L2/OptimismSuperchainERC20.sol": { - "initCodeHash": "0xd7418787c42d9bfdc85f7810deb0f65a014edace04a04f81af218940f0c32a54", - "sourceCodeHash": "0xfd07d6685da601129bd854f79e91363baf9d93d28868add9ca7a0698111158a1" + "initCodeHash": "0x9b057e90bc62b4cd175c685e5317f70cbcb2448258466dc36c563203aceb7fd4", + "sourceCodeHash": "0x0b91f534e4ba21e947fe3f62a5399ae9aa9deb1ea9cfd77a20c7fc95e1c739cd" }, "src/L2/SequencerFeeVault.sol": { "initCodeHash": "0xb94145f571e92ee615c6fe903b6568e8aac5fe760b6b65148ffc45d2fb0f5433", diff --git a/packages/contracts-bedrock/snapshots/abi/OptimismSuperchainERC20.json b/packages/contracts-bedrock/snapshots/abi/OptimismSuperchainERC20.json index eb1fd4b750ec..226ddd0a2857 100644 --- a/packages/contracts-bedrock/snapshots/abi/OptimismSuperchainERC20.json +++ b/packages/contracts-bedrock/snapshots/abi/OptimismSuperchainERC20.json @@ -244,6 +244,11 @@ }, { "inputs": [ + { + "internalType": "address", + "name": "_from", + "type": "address" + }, { "internalType": "address", "name": "_to", @@ -460,6 +465,12 @@ { "anonymous": false, "inputs": [ + { + "indexed": true, + "internalType": "address", + "name": "from", + "type": "address" + }, { "indexed": true, "internalType": "address", @@ -471,6 +482,12 @@ "internalType": "uint256", "name": "amount", "type": "uint256" + }, + { + "indexed": false, + "internalType": "uint256", + "name": "source", + "type": "uint256" } ], "name": "RelayedERC20", @@ -500,7 +517,7 @@ { "indexed": false, "internalType": "uint256", - "name": "chainId", + "name": "destination", "type": "uint256" } ], diff --git a/packages/contracts-bedrock/src/L2/IOptimismSuperchainERC20.sol b/packages/contracts-bedrock/src/L2/IOptimismSuperchainERC20.sol index 3c6ff28dbb93..35948436bc6f 100644 --- a/packages/contracts-bedrock/src/L2/IOptimismSuperchainERC20.sol +++ b/packages/contracts-bedrock/src/L2/IOptimismSuperchainERC20.sol @@ -17,16 +17,18 @@ interface IOptimismSuperchainERC20 { event Burn(address indexed account, uint256 amount); /// @notice Emitted whenever tokens are sent to another chain. - /// @param from Address of the sender. - /// @param to Address of the recipient. - /// @param amount Amount of tokens sent. - /// @param chainId Chain ID of the destination chain. - event SentERC20(address indexed from, address indexed to, uint256 amount, uint256 chainId); + /// @param from Address of the sender. + /// @param to Address of the recipient. + /// @param amount Amount of tokens sent. + /// @param destination Chain ID of the destination chain. + event SentERC20(address indexed from, address indexed to, uint256 amount, uint256 destination); /// @notice Emitted whenever tokens are successfully relayed on this chain. - /// @param to Address of the recipient. - /// @param amount Amount of tokens relayed. - event RelayedERC20(address indexed to, uint256 amount); + /// @param from Address of the sender. + /// @param to Address of the recipient. + /// @param amount Amount of tokens relayed. + /// @param source Chain ID of the source chain. + event RelayedERC20(address indexed from, address indexed to, uint256 amount, uint256 source); /// @notice Allows the L2StandardBridge to mint tokens. /// @param _to Address to mint tokens to. @@ -45,7 +47,8 @@ interface IOptimismSuperchainERC20 { function sendERC20(address _to, uint256 _amount, uint256 _chainId) external; /// @notice Relays tokens received from another chain. - /// @param _to Address to relay tokens to. - /// @param _amount Amount of tokens to relay. - function relayERC20(address _to, uint256 _amount) external; + /// @param _from Address of the sender. + /// @param _to Address to relay tokens to. + /// @param _amount Amount of tokens to relay. + function relayERC20(address _from, address _to, uint256 _amount) external; } diff --git a/packages/contracts-bedrock/src/L2/OptimismSuperchainERC20.sol b/packages/contracts-bedrock/src/L2/OptimismSuperchainERC20.sol index b224703316c5..17ad0ea0cf94 100644 --- a/packages/contracts-bedrock/src/L2/OptimismSuperchainERC20.sol +++ b/packages/contracts-bedrock/src/L2/OptimismSuperchainERC20.sol @@ -100,16 +100,17 @@ contract OptimismSuperchainERC20 is IOptimismSuperchainERC20, ERC20, ISemver { _burn(msg.sender, _amount); - bytes memory _message = abi.encodeCall(this.relayERC20, (_to, _amount)); + bytes memory _message = abi.encodeCall(this.relayERC20, (msg.sender, _to, _amount)); IL2ToL2CrossDomainMessenger(MESSENGER).sendMessage(_chainId, address(this), _message); emit SentERC20(msg.sender, _to, _amount, _chainId); } /// @notice Relays tokens received from another chain. + /// @param _from Address of the sender. /// @param _to Address to relay tokens to. /// @param _amount Amount of tokens to relay. - function relayERC20(address _to, uint256 _amount) external { + function relayERC20(address _from, address _to, uint256 _amount) external { if (_to == address(0)) revert ZeroAddress(); if (msg.sender != MESSENGER) revert CallerNotL2ToL2CrossDomainMessenger(); @@ -117,10 +118,11 @@ contract OptimismSuperchainERC20 is IOptimismSuperchainERC20, ERC20, ISemver { if (IL2ToL2CrossDomainMessenger(MESSENGER).crossDomainMessageSender() != address(this)) { revert InvalidCrossDomainSender(); } + uint256 _source = IL2ToL2CrossDomainMessenger(MESSENGER).crossDomainMessageSource(); _mint(_to, _amount); - emit RelayedERC20(_to, _amount); + emit RelayedERC20(_from, _to, _amount, _source); } /// @notice Returns the number of decimals used to get its user representation. diff --git a/packages/contracts-bedrock/test/L2/OptimismSuperchainERC20.t.sol b/packages/contracts-bedrock/test/L2/OptimismSuperchainERC20.t.sol index ee31b9f87bc7..97a9af3ea310 100644 --- a/packages/contracts-bedrock/test/L2/OptimismSuperchainERC20.t.sol +++ b/packages/contracts-bedrock/test/L2/OptimismSuperchainERC20.t.sol @@ -189,7 +189,7 @@ contract OptimismSuperchainERC20Test is Test { emit IOptimismSuperchainERC20.SentERC20(_sender, _to, _amount, _chainId); // Mock the call over the `sendMessage` function and expect it to be called properly - bytes memory _message = abi.encodeCall(superchainERC20.relayERC20, (_to, _amount)); + bytes memory _message = abi.encodeCall(superchainERC20.relayERC20, (_sender, _to, _amount)); _mockAndExpect( MESSENGER, abi.encodeWithSelector( @@ -218,7 +218,7 @@ contract OptimismSuperchainERC20Test is Test { // Call the `relayERC20` function with the non-messenger caller vm.prank(_caller); - superchainERC20.relayERC20(_to, _amount); + superchainERC20.relayERC20(_caller, _to, _amount); } /// @notice Tests the `relayERC20` function reverts when the `crossDomainMessageSender` that sent the message is not @@ -245,7 +245,7 @@ contract OptimismSuperchainERC20Test is Test { // Call the `relayERC20` function with the sender caller vm.prank(MESSENGER); - superchainERC20.relayERC20(_to, _amount); + superchainERC20.relayERC20(_crossDomainMessageSender, _to, _amount); } /// @notice Tests the `relayERC20` function reverts when the `_to` address is the zero address. @@ -262,11 +262,12 @@ contract OptimismSuperchainERC20Test is Test { // Call the `relayERC20` function with the zero address vm.prank(MESSENGER); - superchainERC20.relayERC20({ _to: ZERO_ADDRESS, _amount: _amount }); + superchainERC20.relayERC20({ _from: ZERO_ADDRESS, _to: ZERO_ADDRESS, _amount: _amount }); } /// @notice Tests the `relayERC20` mints the proper amount and emits the `RelayedERC20` event. - function testFuzz_relayERC20_succeeds(address _to, uint256 _amount) public { + function testFuzz_relayERC20_succeeds(address _from, address _to, uint256 _amount, uint256 _source) public { + vm.assume(_from != ZERO_ADDRESS); vm.assume(_to != ZERO_ADDRESS); // Mock the call over the `crossDomainMessageSender` function setting the same address as value @@ -276,6 +277,13 @@ contract OptimismSuperchainERC20Test is Test { abi.encode(address(superchainERC20)) ); + // Mock the call over the `crossDomainMessageSource` function setting the same address as value + _mockAndExpect( + MESSENGER, + abi.encodeWithSelector(IL2ToL2CrossDomainMessenger.crossDomainMessageSource.selector), + abi.encode(_source) + ); + // Get the total supply and balance of `_to` before the relay to compare later on the assertions uint256 _totalSupplyBefore = superchainERC20.totalSupply(); uint256 _toBalanceBefore = superchainERC20.balanceOf(_to); @@ -286,11 +294,11 @@ contract OptimismSuperchainERC20Test is Test { // Look for the emit of the `RelayedERC20` event vm.expectEmit(true, true, true, true, address(superchainERC20)); - emit IOptimismSuperchainERC20.RelayedERC20(_to, _amount); + emit IOptimismSuperchainERC20.RelayedERC20(_from, _to, _amount, _source); // Call the `relayERC20` function with the messenger caller vm.prank(MESSENGER); - superchainERC20.relayERC20(_to, _amount); + superchainERC20.relayERC20(_from, _to, _amount); // Check the total supply and balance of `_to` after the relay were updated correctly assertEq(superchainERC20.totalSupply(), _totalSupplyBefore + _amount); diff --git a/packages/contracts-bedrock/test/invariants/OptimismSuperchainERC20.t.sol b/packages/contracts-bedrock/test/invariants/OptimismSuperchainERC20.t.sol index 17243db3e7a4..6900c1567d82 100644 --- a/packages/contracts-bedrock/test/invariants/OptimismSuperchainERC20.t.sol +++ b/packages/contracts-bedrock/test/invariants/OptimismSuperchainERC20.t.sol @@ -72,7 +72,7 @@ contract OptimismSuperchainERC20_User is StdUtils { } /// @notice Relay a message from another chain. - function relayMessage() public { + function relayMessage(uint256 _source) public { // Make sure there are unrelayed messages. if (unrelayed.length == 0) { return; @@ -89,10 +89,17 @@ contract OptimismSuperchainERC20_User is StdUtils { abi.encode(address(superchainERC20)) ); + // Simulate the cross-domain message source to any chain. + vm.mockCall( + Predeploys.L2_TO_L2_CROSS_DOMAIN_MESSENGER, + abi.encodeCall(IL2ToL2CrossDomainMessenger.crossDomainMessageSource, ()), + abi.encode(_source) + ); + // Prank the relayERC20 function. // Balance will just go back to our own account. vm.prank(Predeploys.L2_TO_L2_CROSS_DOMAIN_MESSENGER); - try superchainERC20.relayERC20(address(this), message.amount) { + try superchainERC20.relayERC20(address(this), address(this), message.amount) { // Success. } catch { failed = true; From 7dd28e125e5fd945506e59924f8f5d9105e9642f Mon Sep 17 00:00:00 2001 From: agusduha Date: Fri, 26 Jul 2024 17:21:49 -0300 Subject: [PATCH 07/18] fix: typo --- .../contracts-bedrock/test/L2/OptimismSuperchainERC20.t.sol | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/contracts-bedrock/test/L2/OptimismSuperchainERC20.t.sol b/packages/contracts-bedrock/test/L2/OptimismSuperchainERC20.t.sol index 97a9af3ea310..80782a2b486a 100644 --- a/packages/contracts-bedrock/test/L2/OptimismSuperchainERC20.t.sol +++ b/packages/contracts-bedrock/test/L2/OptimismSuperchainERC20.t.sol @@ -277,7 +277,7 @@ contract OptimismSuperchainERC20Test is Test { abi.encode(address(superchainERC20)) ); - // Mock the call over the `crossDomainMessageSource` function setting the same address as value + // Mock the call over the `crossDomainMessageSource` function setting the source chain ID as value _mockAndExpect( MESSENGER, abi.encodeWithSelector(IL2ToL2CrossDomainMessenger.crossDomainMessageSource.selector), From 68bcdca2cfc8d597d2ec4aca479f1ec0fed55001 Mon Sep 17 00:00:00 2001 From: agusduha Date: Fri, 26 Jul 2024 17:35:59 -0300 Subject: [PATCH 08/18] fix: from param description --- packages/contracts-bedrock/semver-lock.json | 2 +- .../contracts-bedrock/src/L2/IOptimismSuperchainERC20.sol | 4 ++-- packages/contracts-bedrock/src/L2/OptimismSuperchainERC20.sol | 2 +- 3 files changed, 4 insertions(+), 4 deletions(-) diff --git a/packages/contracts-bedrock/semver-lock.json b/packages/contracts-bedrock/semver-lock.json index 619a4ddb10f1..5358df8c8d04 100644 --- a/packages/contracts-bedrock/semver-lock.json +++ b/packages/contracts-bedrock/semver-lock.json @@ -105,7 +105,7 @@ }, "src/L2/OptimismSuperchainERC20.sol": { "initCodeHash": "0x9b057e90bc62b4cd175c685e5317f70cbcb2448258466dc36c563203aceb7fd4", - "sourceCodeHash": "0x0b91f534e4ba21e947fe3f62a5399ae9aa9deb1ea9cfd77a20c7fc95e1c739cd" + "sourceCodeHash": "0x20807e9330d23dc3e1524dfd42622c76d13906a9d35aa1ba0fce544c4609c224" }, "src/L2/SequencerFeeVault.sol": { "initCodeHash": "0xb94145f571e92ee615c6fe903b6568e8aac5fe760b6b65148ffc45d2fb0f5433", diff --git a/packages/contracts-bedrock/src/L2/IOptimismSuperchainERC20.sol b/packages/contracts-bedrock/src/L2/IOptimismSuperchainERC20.sol index 35948436bc6f..c262306ce518 100644 --- a/packages/contracts-bedrock/src/L2/IOptimismSuperchainERC20.sol +++ b/packages/contracts-bedrock/src/L2/IOptimismSuperchainERC20.sol @@ -24,7 +24,7 @@ interface IOptimismSuperchainERC20 { event SentERC20(address indexed from, address indexed to, uint256 amount, uint256 destination); /// @notice Emitted whenever tokens are successfully relayed on this chain. - /// @param from Address of the sender. + /// @param from Address of the msg.sender of sendERC20 on the source chain. /// @param to Address of the recipient. /// @param amount Amount of tokens relayed. /// @param source Chain ID of the source chain. @@ -47,7 +47,7 @@ interface IOptimismSuperchainERC20 { function sendERC20(address _to, uint256 _amount, uint256 _chainId) external; /// @notice Relays tokens received from another chain. - /// @param _from Address of the sender. + /// @param _from Address of the msg.sender of sendERC20 on the source chain. /// @param _to Address to relay tokens to. /// @param _amount Amount of tokens to relay. function relayERC20(address _from, address _to, uint256 _amount) external; diff --git a/packages/contracts-bedrock/src/L2/OptimismSuperchainERC20.sol b/packages/contracts-bedrock/src/L2/OptimismSuperchainERC20.sol index 17ad0ea0cf94..9e76d17911e4 100644 --- a/packages/contracts-bedrock/src/L2/OptimismSuperchainERC20.sol +++ b/packages/contracts-bedrock/src/L2/OptimismSuperchainERC20.sol @@ -107,7 +107,7 @@ contract OptimismSuperchainERC20 is IOptimismSuperchainERC20, ERC20, ISemver { } /// @notice Relays tokens received from another chain. - /// @param _from Address of the sender. + /// @param _from Address of the msg.sender of sendERC20 on the source chain. /// @param _to Address to relay tokens to. /// @param _amount Amount of tokens to relay. function relayERC20(address _from, address _to, uint256 _amount) external { From ca487dcf60cb42cc8151ec46063564b4adca2027 Mon Sep 17 00:00:00 2001 From: 0xng Date: Mon, 29 Jul 2024 12:53:00 -0300 Subject: [PATCH 09/18] fix: event signature and interface pragma --- packages/contracts-bedrock/.gas-snapshot | 8 ++++---- packages/contracts-bedrock/semver-lock.json | 4 ++-- .../snapshots/abi/OptimismSuperchainERC20.json | 2 +- .../contracts-bedrock/src/L2/IOptimismSuperchainERC20.sol | 4 ++-- .../contracts-bedrock/src/L2/OptimismSuperchainERC20.sol | 2 +- .../test/L2/OptimismSuperchainERC20.t.sol | 6 +++--- 6 files changed, 13 insertions(+), 13 deletions(-) diff --git a/packages/contracts-bedrock/.gas-snapshot b/packages/contracts-bedrock/.gas-snapshot index a97f05678b58..b3ea3b88545e 100644 --- a/packages/contracts-bedrock/.gas-snapshot +++ b/packages/contracts-bedrock/.gas-snapshot @@ -1,7 +1,7 @@ -GasBenchMark_L1CrossDomainMessenger:test_sendMessage_benchmark_0() (gas: 369380) -GasBenchMark_L1CrossDomainMessenger:test_sendMessage_benchmark_1() (gas: 2967520) -GasBenchMark_L1StandardBridge_Deposit:test_depositERC20_benchmark_0() (gas: 561992) -GasBenchMark_L1StandardBridge_Deposit:test_depositERC20_benchmark_1() (gas: 4074035) +GasBenchMark_L1CrossDomainMessenger:test_sendMessage_benchmark_0() (gas: 369356) +GasBenchMark_L1CrossDomainMessenger:test_sendMessage_benchmark_1() (gas: 2967496) +GasBenchMark_L1StandardBridge_Deposit:test_depositERC20_benchmark_0() (gas: 564483) +GasBenchMark_L1StandardBridge_Deposit:test_depositERC20_benchmark_1() (gas: 4076526) GasBenchMark_L1StandardBridge_Deposit:test_depositETH_benchmark_0() (gas: 466947) GasBenchMark_L1StandardBridge_Deposit:test_depositETH_benchmark_1() (gas: 3512629) GasBenchMark_L1StandardBridge_Finalize:test_finalizeETHWithdrawal_benchmark() (gas: 72624) diff --git a/packages/contracts-bedrock/semver-lock.json b/packages/contracts-bedrock/semver-lock.json index 5358df8c8d04..0f3ca4031d86 100644 --- a/packages/contracts-bedrock/semver-lock.json +++ b/packages/contracts-bedrock/semver-lock.json @@ -104,8 +104,8 @@ "sourceCodeHash": "0x1f14aafab2cb15970cccedb461b72218fca8afa6ffd0ac696a9e28ff1415a068" }, "src/L2/OptimismSuperchainERC20.sol": { - "initCodeHash": "0x9b057e90bc62b4cd175c685e5317f70cbcb2448258466dc36c563203aceb7fd4", - "sourceCodeHash": "0x20807e9330d23dc3e1524dfd42622c76d13906a9d35aa1ba0fce544c4609c224" + "initCodeHash": "0xcf9b0aa1e3a042ee6860de68ea27fa4f812373053fa320626810270a59ac8d6a", + "sourceCodeHash": "0xde9084c991e7c6f5c5c7f56c26c9a20871e16b31c8a5dd7c4958c66fbc7302e2" }, "src/L2/SequencerFeeVault.sol": { "initCodeHash": "0xb94145f571e92ee615c6fe903b6568e8aac5fe760b6b65148ffc45d2fb0f5433", diff --git a/packages/contracts-bedrock/snapshots/abi/OptimismSuperchainERC20.json b/packages/contracts-bedrock/snapshots/abi/OptimismSuperchainERC20.json index 226ddd0a2857..2f2662b2b333 100644 --- a/packages/contracts-bedrock/snapshots/abi/OptimismSuperchainERC20.json +++ b/packages/contracts-bedrock/snapshots/abi/OptimismSuperchainERC20.json @@ -521,7 +521,7 @@ "type": "uint256" } ], - "name": "SentERC20", + "name": "SendERC20", "type": "event" }, { diff --git a/packages/contracts-bedrock/src/L2/IOptimismSuperchainERC20.sol b/packages/contracts-bedrock/src/L2/IOptimismSuperchainERC20.sol index c262306ce518..e202fa9e1e27 100644 --- a/packages/contracts-bedrock/src/L2/IOptimismSuperchainERC20.sol +++ b/packages/contracts-bedrock/src/L2/IOptimismSuperchainERC20.sol @@ -1,5 +1,5 @@ // SPDX-License-Identifier: MIT -pragma solidity 0.8.25; +pragma solidity ^0.8.0; /// @title IOptimismSuperchainERC20 /// @notice This interface is available on the OptimismSuperchainERC20 contract. @@ -21,7 +21,7 @@ interface IOptimismSuperchainERC20 { /// @param to Address of the recipient. /// @param amount Amount of tokens sent. /// @param destination Chain ID of the destination chain. - event SentERC20(address indexed from, address indexed to, uint256 amount, uint256 destination); + event SendERC20(address indexed from, address indexed to, uint256 amount, uint256 destination); /// @notice Emitted whenever tokens are successfully relayed on this chain. /// @param from Address of the msg.sender of sendERC20 on the source chain. diff --git a/packages/contracts-bedrock/src/L2/OptimismSuperchainERC20.sol b/packages/contracts-bedrock/src/L2/OptimismSuperchainERC20.sol index 9e76d17911e4..4674505b5412 100644 --- a/packages/contracts-bedrock/src/L2/OptimismSuperchainERC20.sol +++ b/packages/contracts-bedrock/src/L2/OptimismSuperchainERC20.sol @@ -103,7 +103,7 @@ contract OptimismSuperchainERC20 is IOptimismSuperchainERC20, ERC20, ISemver { bytes memory _message = abi.encodeCall(this.relayERC20, (msg.sender, _to, _amount)); IL2ToL2CrossDomainMessenger(MESSENGER).sendMessage(_chainId, address(this), _message); - emit SentERC20(msg.sender, _to, _amount, _chainId); + emit SendERC20(msg.sender, _to, _amount, _chainId); } /// @notice Relays tokens received from another chain. diff --git a/packages/contracts-bedrock/test/L2/OptimismSuperchainERC20.t.sol b/packages/contracts-bedrock/test/L2/OptimismSuperchainERC20.t.sol index 80782a2b486a..f64007c15dbf 100644 --- a/packages/contracts-bedrock/test/L2/OptimismSuperchainERC20.t.sol +++ b/packages/contracts-bedrock/test/L2/OptimismSuperchainERC20.t.sol @@ -165,7 +165,7 @@ contract OptimismSuperchainERC20Test is Test { superchainERC20.sendERC20({ _to: ZERO_ADDRESS, _amount: _amount, _chainId: _chainId }); } - /// @notice Tests the `sendERC20` function burns the sender tokens, sends the message, and emits the `SentERC20` + /// @notice Tests the `sendERC20` function burns the sender tokens, sends the message, and emits the `SendERC20` /// event. function testFuzz_sendERC20_succeeds(address _sender, address _to, uint256 _amount, uint256 _chainId) external { // Ensure `_sender` is not the zero address @@ -184,9 +184,9 @@ contract OptimismSuperchainERC20Test is Test { vm.expectEmit(true, true, true, true, address(superchainERC20)); emit IERC20.Transfer(_sender, ZERO_ADDRESS, _amount); - // Look for the emit of the `SentERC20` event + // Look for the emit of the `SendERC20` event vm.expectEmit(true, true, true, true, address(superchainERC20)); - emit IOptimismSuperchainERC20.SentERC20(_sender, _to, _amount, _chainId); + emit IOptimismSuperchainERC20.SendERC20(_sender, _to, _amount, _chainId); // Mock the call over the `sendMessage` function and expect it to be called properly bytes memory _message = abi.encodeCall(superchainERC20.relayERC20, (_sender, _to, _amount)); From 475d83d47fdd28eb62b4df0adba3f76b06729212 Mon Sep 17 00:00:00 2001 From: agusduha Date: Wed, 31 Jul 2024 11:40:14 -0300 Subject: [PATCH 10/18] feat: add initializer --- packages/contracts-bedrock/.gas-snapshot | 8 +- packages/contracts-bedrock/semver-lock.json | 4 +- .../abi/OptimismSuperchainERC20.json | 100 ++++++++++++------ .../OptimismSuperchainERC20.json | 18 +++- .../src/L2/IOptimismSuperchainERC20.sol | 3 + .../src/L2/OptimismSuperchainERC20.sol | 64 +++++++---- .../test/L2/OptimismSuperchainERC20.t.sol | 58 ++++++++-- .../invariants/OptimismSuperchainERC20.t.sol | 14 ++- 8 files changed, 192 insertions(+), 77 deletions(-) diff --git a/packages/contracts-bedrock/.gas-snapshot b/packages/contracts-bedrock/.gas-snapshot index b3ea3b88545e..a97f05678b58 100644 --- a/packages/contracts-bedrock/.gas-snapshot +++ b/packages/contracts-bedrock/.gas-snapshot @@ -1,7 +1,7 @@ -GasBenchMark_L1CrossDomainMessenger:test_sendMessage_benchmark_0() (gas: 369356) -GasBenchMark_L1CrossDomainMessenger:test_sendMessage_benchmark_1() (gas: 2967496) -GasBenchMark_L1StandardBridge_Deposit:test_depositERC20_benchmark_0() (gas: 564483) -GasBenchMark_L1StandardBridge_Deposit:test_depositERC20_benchmark_1() (gas: 4076526) +GasBenchMark_L1CrossDomainMessenger:test_sendMessage_benchmark_0() (gas: 369380) +GasBenchMark_L1CrossDomainMessenger:test_sendMessage_benchmark_1() (gas: 2967520) +GasBenchMark_L1StandardBridge_Deposit:test_depositERC20_benchmark_0() (gas: 561992) +GasBenchMark_L1StandardBridge_Deposit:test_depositERC20_benchmark_1() (gas: 4074035) GasBenchMark_L1StandardBridge_Deposit:test_depositETH_benchmark_0() (gas: 466947) GasBenchMark_L1StandardBridge_Deposit:test_depositETH_benchmark_1() (gas: 3512629) GasBenchMark_L1StandardBridge_Finalize:test_finalizeETHWithdrawal_benchmark() (gas: 72624) diff --git a/packages/contracts-bedrock/semver-lock.json b/packages/contracts-bedrock/semver-lock.json index 0f3ca4031d86..4fc766b23116 100644 --- a/packages/contracts-bedrock/semver-lock.json +++ b/packages/contracts-bedrock/semver-lock.json @@ -104,8 +104,8 @@ "sourceCodeHash": "0x1f14aafab2cb15970cccedb461b72218fca8afa6ffd0ac696a9e28ff1415a068" }, "src/L2/OptimismSuperchainERC20.sol": { - "initCodeHash": "0xcf9b0aa1e3a042ee6860de68ea27fa4f812373053fa320626810270a59ac8d6a", - "sourceCodeHash": "0xde9084c991e7c6f5c5c7f56c26c9a20871e16b31c8a5dd7c4958c66fbc7302e2" + "initCodeHash": "0x3ca4452dc167b6705a017016c9f1bf7f765cf871efdac56707735d6b9354d5e8", + "sourceCodeHash": "0x99861db89dde9af91df484816d35b4fd29b612d08cb6ef242699f627756602c4" }, "src/L2/SequencerFeeVault.sol": { "initCodeHash": "0xb94145f571e92ee615c6fe903b6568e8aac5fe760b6b65148ffc45d2fb0f5433", diff --git a/packages/contracts-bedrock/snapshots/abi/OptimismSuperchainERC20.json b/packages/contracts-bedrock/snapshots/abi/OptimismSuperchainERC20.json index 2f2662b2b333..a3368d1494f7 100644 --- a/packages/contracts-bedrock/snapshots/abi/OptimismSuperchainERC20.json +++ b/packages/contracts-bedrock/snapshots/abi/OptimismSuperchainERC20.json @@ -1,27 +1,6 @@ [ { - "inputs": [ - { - "internalType": "address", - "name": "_remoteToken", - "type": "address" - }, - { - "internalType": "string", - "name": "_tokenName", - "type": "string" - }, - { - "internalType": "string", - "name": "_tokenSymbol", - "type": "string" - }, - { - "internalType": "uint8", - "name": "_decimals", - "type": "uint8" - } - ], + "inputs": [], "stateMutability": "nonpayable", "type": "constructor" }, @@ -38,19 +17,6 @@ "stateMutability": "view", "type": "function" }, - { - "inputs": [], - "name": "REMOTE_TOKEN", - "outputs": [ - { - "internalType": "address", - "name": "", - "type": "address" - } - ], - "stateMutability": "view", - "type": "function" - }, { "inputs": [ { @@ -149,6 +115,34 @@ "stateMutability": "view", "type": "function" }, + { + "inputs": [ + { + "internalType": "address", + "name": "__remoteToken", + "type": "address" + }, + { + "internalType": "string", + "name": "__name", + "type": "string" + }, + { + "internalType": "string", + "name": "__symbol", + "type": "string" + }, + { + "internalType": "uint8", + "name": "__decimals", + "type": "uint8" + } + ], + "name": "initialize", + "outputs": [], + "stateMutability": "nonpayable", + "type": "function" + }, { "inputs": [ { @@ -265,6 +259,19 @@ "stateMutability": "nonpayable", "type": "function" }, + { + "inputs": [], + "name": "remoteToken", + "outputs": [ + { + "internalType": "address", + "name": "", + "type": "address" + } + ], + "stateMutability": "view", + "type": "function" + }, { "inputs": [ { @@ -443,6 +450,19 @@ "name": "Burn", "type": "event" }, + { + "anonymous": false, + "inputs": [ + { + "indexed": false, + "internalType": "uint64", + "name": "version", + "type": "uint64" + } + ], + "name": "Initialized", + "type": "event" + }, { "anonymous": false, "inputs": [ @@ -579,11 +599,21 @@ "name": "InvalidCrossDomainSender", "type": "error" }, + { + "inputs": [], + "name": "InvalidInitialization", + "type": "error" + }, { "inputs": [], "name": "InvalidPermit", "type": "error" }, + { + "inputs": [], + "name": "NotInitializing", + "type": "error" + }, { "inputs": [], "name": "OnlyBridge", diff --git a/packages/contracts-bedrock/snapshots/storageLayout/OptimismSuperchainERC20.json b/packages/contracts-bedrock/snapshots/storageLayout/OptimismSuperchainERC20.json index 76bff10681c2..ddbdcc8db1d7 100644 --- a/packages/contracts-bedrock/snapshots/storageLayout/OptimismSuperchainERC20.json +++ b/packages/contracts-bedrock/snapshots/storageLayout/OptimismSuperchainERC20.json @@ -1,16 +1,30 @@ [ + { + "bytes": "20", + "label": "_remoteToken", + "offset": 0, + "slot": "0", + "type": "address" + }, { "bytes": "32", "label": "_name", "offset": 0, - "slot": "0", + "slot": "1", "type": "string" }, { "bytes": "32", "label": "_symbol", "offset": 0, - "slot": "1", + "slot": "2", "type": "string" + }, + { + "bytes": "1", + "label": "_decimals", + "offset": 0, + "slot": "3", + "type": "uint8" } ] \ No newline at end of file diff --git a/packages/contracts-bedrock/src/L2/IOptimismSuperchainERC20.sol b/packages/contracts-bedrock/src/L2/IOptimismSuperchainERC20.sol index e202fa9e1e27..81a7f668d91e 100644 --- a/packages/contracts-bedrock/src/L2/IOptimismSuperchainERC20.sol +++ b/packages/contracts-bedrock/src/L2/IOptimismSuperchainERC20.sol @@ -51,4 +51,7 @@ interface IOptimismSuperchainERC20 { /// @param _to Address to relay tokens to. /// @param _amount Amount of tokens to relay. function relayERC20(address _from, address _to, uint256 _amount) external; + + /// @notice Returns the address of the corresponding remote token. + function remoteToken() external view returns (address); } diff --git a/packages/contracts-bedrock/src/L2/OptimismSuperchainERC20.sol b/packages/contracts-bedrock/src/L2/OptimismSuperchainERC20.sol index 4674505b5412..5e8d155225b2 100644 --- a/packages/contracts-bedrock/src/L2/OptimismSuperchainERC20.sol +++ b/packages/contracts-bedrock/src/L2/OptimismSuperchainERC20.sol @@ -6,6 +6,7 @@ import { ERC20 } from "@solady/tokens/ERC20.sol"; import { IL2ToL2CrossDomainMessenger } from "src/L2/IL2ToL2CrossDomainMessenger.sol"; import { ISemver } from "src/universal/ISemver.sol"; import { Predeploys } from "src/libraries/Predeploys.sol"; +import { Initializable } from "@solady/utils/Initializable.sol"; /// @notice Thrown when attempting to relay a message and the function caller (msg.sender) is not /// L2ToL2CrossDomainMessenger. @@ -29,7 +30,7 @@ error ZeroAddress(); /// 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 IOptimismSuperchainERC20, ERC20, ISemver { +contract OptimismSuperchainERC20 is IOptimismSuperchainERC20, ERC20, ISemver, Initializable { /// @notice Address of the L2ToL2CrossDomainMessenger Predeploy. address internal constant MESSENGER = Predeploys.L2_TO_L2_CROSS_DOMAIN_MESSENGER; @@ -37,10 +38,7 @@ contract OptimismSuperchainERC20 is IOptimismSuperchainERC20, ERC20, ISemver { address internal constant BRIDGE = Predeploys.L2_STANDARD_BRIDGE; /// @notice Address of the corresponding version of this token on the remote chain. - address public immutable REMOTE_TOKEN; - - /// @notice Decimals of the token - uint8 private immutable DECIMALS; + address private _remoteToken; /// @notice Name of the token string private _name; @@ -48,6 +46,9 @@ contract OptimismSuperchainERC20 is IOptimismSuperchainERC20, ERC20, ISemver { /// @notice Symbol of the token string private _symbol; + /// @notice Decimals of the token + uint8 private _decimals; + /// @notice A modifier that only allows the bridge to call modifier onlyBridge() { if (msg.sender != BRIDGE) revert OnlyBridge(); @@ -58,15 +59,29 @@ contract OptimismSuperchainERC20 is IOptimismSuperchainERC20, ERC20, ISemver { /// @custom:semver 1.0.0-beta.1 string public constant version = "1.0.0-beta.1"; - /// @param _remoteToken Address of the corresponding remote token. - /// @param _tokenName ERC20 name. - /// @param _tokenSymbol ERC20 symbol. - /// @param _decimals ERC20 decimals. - constructor(address _remoteToken, string memory _tokenName, string memory _tokenSymbol, uint8 _decimals) { - REMOTE_TOKEN = _remoteToken; - DECIMALS = _decimals; - _name = _tokenName; - _symbol = _tokenSymbol; + /// @notice Constructs the OptimismSuperchainERC20 contract. + constructor() { + _disableInitializers(); + } + + /// @notice Initializes the contract. + /// @param __remoteToken Address of the corresponding remote token. + /// @param __name ERC20 name. + /// @param __symbol ERC20 symbol. + /// @param __decimals ERC20 decimals. + function initialize( + address __remoteToken, + string memory __name, + string memory __symbol, + uint8 __decimals + ) + external + initializer + { + _remoteToken = __remoteToken; + _name = __name; + _symbol = __symbol; + _decimals = __decimals; } /// @notice Allows the L2StandardBridge to mint tokens. @@ -125,14 +140,9 @@ contract OptimismSuperchainERC20 is IOptimismSuperchainERC20, ERC20, ISemver { emit RelayedERC20(_from, _to, _amount, _source); } - /// @notice 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; + /// @notice Returns the address of the corresponding remote token. + function remoteToken() public view override returns (address) { + return _remoteToken; } /// @notice Returns the name of the token. @@ -145,6 +155,16 @@ contract OptimismSuperchainERC20 is IOptimismSuperchainERC20, ERC20, ISemver { return _symbol; } + /// @notice 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; + } + /// @notice ERC165 interface check function. /// @param _interfaceId Interface ID to check. /// @return Whether or not the interface is supported by this contract. diff --git a/packages/contracts-bedrock/test/L2/OptimismSuperchainERC20.t.sol b/packages/contracts-bedrock/test/L2/OptimismSuperchainERC20.t.sol index f64007c15dbf..0dfcafdf41eb 100644 --- a/packages/contracts-bedrock/test/L2/OptimismSuperchainERC20.t.sol +++ b/packages/contracts-bedrock/test/L2/OptimismSuperchainERC20.t.sol @@ -10,6 +10,8 @@ import { IERC20 } from "@openzeppelin/contracts/token/ERC20/IERC20.sol"; import { stdStorage, StdStorage } from "forge-std/Test.sol"; import { ERC20 } from "@openzeppelin/contracts/token/ERC20/ERC20.sol"; import { IL2ToL2CrossDomainMessenger } from "src/L2/IL2ToL2CrossDomainMessenger.sol"; +import { ERC1967Proxy } from "@openzeppelin/contracts/proxy/ERC1967/ERC1967Proxy.sol"; +import { Initializable } from "@solady/utils/Initializable.sol"; // Target contract import { @@ -32,11 +34,33 @@ contract OptimismSuperchainERC20Test is Test { address internal constant BRIDGE = Predeploys.L2_STANDARD_BRIDGE; address internal constant MESSENGER = Predeploys.L2_TO_L2_CROSS_DOMAIN_MESSENGER; + OptimismSuperchainERC20 public superchainERC20Impl; OptimismSuperchainERC20 public superchainERC20; /// @notice Sets up the test suite. function setUp() public { - superchainERC20 = new OptimismSuperchainERC20(REMOTE_TOKEN, NAME, SYMBOL, DECIMALS); + superchainERC20Impl = new OptimismSuperchainERC20(); + superchainERC20 = _deploySuperchainERC20Proxy(REMOTE_TOKEN, NAME, SYMBOL, DECIMALS); + } + + /// @notice Helper function to deploy a proxy of the OptimismSuperchainERC20 contract. + function _deploySuperchainERC20Proxy( + address _remoteToken, + string memory _name, + string memory _symbol, + uint8 _decimals + ) + internal + returns (OptimismSuperchainERC20) + { + return OptimismSuperchainERC20( + address( + new ERC1967Proxy( + address(superchainERC20Impl), + abi.encodeCall(OptimismSuperchainERC20.initialize, (_remoteToken, _name, _symbol, _decimals)) + ) + ) + ); } /// @notice Helper function to setup a mock and expect a call to it. @@ -45,12 +69,28 @@ contract OptimismSuperchainERC20Test is Test { vm.expectCall(_receiver, _calldata); } - /// @notice Test that the bridge's constructor sets the correct values. - function test_constructor_succeeds() public view { + /// @notice Test that the contract's `initializer` sets the correct values. + function test_initializer_succeeds() public view { assertEq(superchainERC20.name(), NAME); assertEq(superchainERC20.symbol(), SYMBOL); assertEq(superchainERC20.decimals(), DECIMALS); - assertEq(superchainERC20.REMOTE_TOKEN(), REMOTE_TOKEN); + assertEq(superchainERC20.remoteToken(), REMOTE_TOKEN); + } + + /// @notice Tests the `initialize` function reverts when the contract is already initialized. + function testFuzz_initializer_reverts( + address _remoteToken, + string memory _name, + string memory _symbol, + uint8 _decimals + ) + public + { + // Expect the revert with `InvalidInitialization` selector + vm.expectRevert(Initializable.InvalidInitialization.selector); + + // Call the `initialize` function again + superchainERC20.initialize(_remoteToken, _name, _symbol, _decimals); } /// @notice Tests the `mint` function reverts when the caller is not the bridge. @@ -307,25 +347,25 @@ contract OptimismSuperchainERC20Test is Test { /// @notice Tests the `decimals` function always returns the correct value. function testFuzz_decimals_succeeds(uint8 _decimals) public { - OptimismSuperchainERC20 _newSuperchainERC20 = new OptimismSuperchainERC20(REMOTE_TOKEN, NAME, SYMBOL, _decimals); + OptimismSuperchainERC20 _newSuperchainERC20 = _deploySuperchainERC20Proxy(REMOTE_TOKEN, NAME, SYMBOL, _decimals); assertEq(_newSuperchainERC20.decimals(), _decimals); } /// @notice Tests the `REMOTE_TOKEN` function always returns the correct value. function testFuzz_remoteToken_succeeds(address _remoteToken) public { - OptimismSuperchainERC20 _newSuperchainERC20 = new OptimismSuperchainERC20(_remoteToken, NAME, SYMBOL, DECIMALS); - assertEq(_newSuperchainERC20.REMOTE_TOKEN(), _remoteToken); + OptimismSuperchainERC20 _newSuperchainERC20 = _deploySuperchainERC20Proxy(_remoteToken, NAME, SYMBOL, DECIMALS); + assertEq(_newSuperchainERC20.remoteToken(), _remoteToken); } /// @notice Tests the `name` function always returns the correct value. function testFuzz_name_succeeds(string memory _name) public { - OptimismSuperchainERC20 _newSuperchainERC20 = new OptimismSuperchainERC20(REMOTE_TOKEN, _name, SYMBOL, DECIMALS); + OptimismSuperchainERC20 _newSuperchainERC20 = _deploySuperchainERC20Proxy(REMOTE_TOKEN, _name, SYMBOL, DECIMALS); assertEq(_newSuperchainERC20.name(), _name); } /// @notice Tests the `symbol` function always returns the correct value. function testFuzz_symbol_succeeds(string memory _symbol) public { - OptimismSuperchainERC20 _newSuperchainERC20 = new OptimismSuperchainERC20(REMOTE_TOKEN, NAME, _symbol, DECIMALS); + OptimismSuperchainERC20 _newSuperchainERC20 = _deploySuperchainERC20Proxy(REMOTE_TOKEN, NAME, _symbol, DECIMALS); assertEq(_newSuperchainERC20.symbol(), _symbol); } diff --git a/packages/contracts-bedrock/test/invariants/OptimismSuperchainERC20.t.sol b/packages/contracts-bedrock/test/invariants/OptimismSuperchainERC20.t.sol index 6900c1567d82..cd86ca33c44c 100644 --- a/packages/contracts-bedrock/test/invariants/OptimismSuperchainERC20.t.sol +++ b/packages/contracts-bedrock/test/invariants/OptimismSuperchainERC20.t.sol @@ -117,6 +117,9 @@ contract OptimismSuperchainERC20_SendSucceeds_Invariant is Test { /// @notice Starting balance of the contract. uint256 internal constant STARTING_BALANCE = type(uint256).max; + /// @notice The OptimismSuperchainERC20 contract implementation. + address internal optimismSuperchainERC20Impl; + /// @notice The OptimismSuperchainERC20_User actor. OptimismSuperchainERC20_User internal actor; @@ -129,8 +132,13 @@ contract OptimismSuperchainERC20_SendSucceeds_Invariant is Test { address _impl = _setImplementationCode(Predeploys.L2_TO_L2_CROSS_DOMAIN_MESSENGER); _setProxyCode(Predeploys.L2_TO_L2_CROSS_DOMAIN_MESSENGER, _impl); - // Create a new OptimismSuperchainERC20 - optimismSuperchainERC20 = new OptimismSuperchainERC20(address(0x123), "Supertoken", "SUP", 18); + // Create a new OptimismSuperchainERC20 implementation. + optimismSuperchainERC20Impl = address(new OptimismSuperchainERC20()); + + // Deploy the OptimismSuperchainERC20 contract. + address _proxy = address(0x123456); + _setProxyCode(_proxy, optimismSuperchainERC20Impl); + optimismSuperchainERC20 = OptimismSuperchainERC20(_proxy); // Create a new OptimismSuperchainERC20_User actor. actor = new OptimismSuperchainERC20_User(vm, optimismSuperchainERC20, STARTING_BALANCE); @@ -156,7 +164,7 @@ contract OptimismSuperchainERC20_SendSucceeds_Invariant is Test { /// @notice Sets the bytecode in the proxy address. function _setProxyCode(address _addr, address _impl) internal { - bytes memory code = vm.getDeployedCode("Proxy.sol:Proxy"); + bytes memory code = vm.getDeployedCode("universal/Proxy.sol:Proxy"); vm.etch(_addr, code); EIP1967Helper.setAdmin(_addr, Predeploys.PROXY_ADMIN); EIP1967Helper.setImplementation(_addr, _impl); From a7fdcf677072381e4a796b01ddda9975ef1bab82 Mon Sep 17 00:00:00 2001 From: agusduha Date: Thu, 1 Aug 2024 11:28:48 -0300 Subject: [PATCH 11/18] feat: use unstructured storage and OZ v5 --- .gitmodules | 3 + packages/contracts-bedrock/foundry.toml | 1 + .../invariant-docs/OptimismSuperchainERC20.md | 2 +- .../lib/openzeppelin-contracts-v5 | 1 + packages/contracts-bedrock/semver-lock.json | 4 +- .../abi/OptimismSuperchainERC20.json | 8 +-- .../OptimismSuperchainERC20.json | 31 +-------- .../src/L2/IOptimismSuperchainERC20.sol | 2 +- .../src/L2/OptimismSuperchainERC20.sol | 69 +++++++++++-------- .../test/L2/OptimismSuperchainERC20.t.sol | 2 +- .../test/vendor/Initializable.t.sol | 3 +- 11 files changed, 58 insertions(+), 68 deletions(-) create mode 160000 packages/contracts-bedrock/lib/openzeppelin-contracts-v5 diff --git a/.gitmodules b/.gitmodules index f2b84783c409..21ecaedbb77a 100644 --- a/.gitmodules +++ b/.gitmodules @@ -26,3 +26,6 @@ [submodule "packages/contracts-bedrock/lib/automate"] path = packages/contracts-bedrock/lib/automate url = https://github.com/gelatodigital/automate +[submodule "packages/contracts-bedrock/lib/openzeppelin-contracts-v5"] + path = packages/contracts-bedrock/lib/openzeppelin-contracts-v5 + url = https://github.com/OpenZeppelin/openzeppelin-contracts diff --git a/packages/contracts-bedrock/foundry.toml b/packages/contracts-bedrock/foundry.toml index 4b1dbdeba780..a25c1fd66fdf 100644 --- a/packages/contracts-bedrock/foundry.toml +++ b/packages/contracts-bedrock/foundry.toml @@ -13,6 +13,7 @@ optimizer_runs = 999999 remappings = [ '@openzeppelin/contracts-upgradeable/=lib/openzeppelin-contracts-upgradeable/contracts', '@openzeppelin/contracts/=lib/openzeppelin-contracts/contracts', + '@openzeppelin/contracts-v5/=lib/openzeppelin-contracts-v5/contracts', '@rari-capital/solmate/=lib/solmate', '@lib-keccak/=lib/lib-keccak/contracts/lib', '@solady/=lib/solady/src', diff --git a/packages/contracts-bedrock/invariant-docs/OptimismSuperchainERC20.md b/packages/contracts-bedrock/invariant-docs/OptimismSuperchainERC20.md index 5bdbd57a4d19..6d2904fd3e75 100644 --- a/packages/contracts-bedrock/invariant-docs/OptimismSuperchainERC20.md +++ b/packages/contracts-bedrock/invariant-docs/OptimismSuperchainERC20.md @@ -1,5 +1,5 @@ # `OptimismSuperchainERC20` Invariants ## Calls to sendERC20 should always succeed as long as the actor has enough balance. Actor's balance should also not increase out of nowhere. -**Test:** [`OptimismSuperchainERC20.t.sol#L150`](../test/invariants/OptimismSuperchainERC20.t.sol#L150) +**Test:** [`OptimismSuperchainERC20.t.sol#L177`](../test/invariants/OptimismSuperchainERC20.t.sol#L177) diff --git a/packages/contracts-bedrock/lib/openzeppelin-contracts-v5 b/packages/contracts-bedrock/lib/openzeppelin-contracts-v5 new file mode 160000 index 000000000000..dbb6104ce834 --- /dev/null +++ b/packages/contracts-bedrock/lib/openzeppelin-contracts-v5 @@ -0,0 +1 @@ +Subproject commit dbb6104ce834628e473d2173bbc9d47f81a9eec3 diff --git a/packages/contracts-bedrock/semver-lock.json b/packages/contracts-bedrock/semver-lock.json index 4fc766b23116..f56a4549a6ea 100644 --- a/packages/contracts-bedrock/semver-lock.json +++ b/packages/contracts-bedrock/semver-lock.json @@ -104,8 +104,8 @@ "sourceCodeHash": "0x1f14aafab2cb15970cccedb461b72218fca8afa6ffd0ac696a9e28ff1415a068" }, "src/L2/OptimismSuperchainERC20.sol": { - "initCodeHash": "0x3ca4452dc167b6705a017016c9f1bf7f765cf871efdac56707735d6b9354d5e8", - "sourceCodeHash": "0x99861db89dde9af91df484816d35b4fd29b612d08cb6ef242699f627756602c4" + "initCodeHash": "0xbfa1d35644a8b97c657101289e64b92b8faeed0b0090195eb871da2de4583bb3", + "sourceCodeHash": "0x56bed1f02792ef9513fe209552e964c983a8f29dbcb2b8a76910fdae929f30ff" }, "src/L2/SequencerFeeVault.sol": { "initCodeHash": "0xb94145f571e92ee615c6fe903b6568e8aac5fe760b6b65148ffc45d2fb0f5433", diff --git a/packages/contracts-bedrock/snapshots/abi/OptimismSuperchainERC20.json b/packages/contracts-bedrock/snapshots/abi/OptimismSuperchainERC20.json index a3368d1494f7..dd05c3f2bbf2 100644 --- a/packages/contracts-bedrock/snapshots/abi/OptimismSuperchainERC20.json +++ b/packages/contracts-bedrock/snapshots/abi/OptimismSuperchainERC20.json @@ -119,22 +119,22 @@ "inputs": [ { "internalType": "address", - "name": "__remoteToken", + "name": "_remoteToken", "type": "address" }, { "internalType": "string", - "name": "__name", + "name": "_name", "type": "string" }, { "internalType": "string", - "name": "__symbol", + "name": "_symbol", "type": "string" }, { "internalType": "uint8", - "name": "__decimals", + "name": "_decimals", "type": "uint8" } ], diff --git a/packages/contracts-bedrock/snapshots/storageLayout/OptimismSuperchainERC20.json b/packages/contracts-bedrock/snapshots/storageLayout/OptimismSuperchainERC20.json index ddbdcc8db1d7..0637a088a01e 100644 --- a/packages/contracts-bedrock/snapshots/storageLayout/OptimismSuperchainERC20.json +++ b/packages/contracts-bedrock/snapshots/storageLayout/OptimismSuperchainERC20.json @@ -1,30 +1 @@ -[ - { - "bytes": "20", - "label": "_remoteToken", - "offset": 0, - "slot": "0", - "type": "address" - }, - { - "bytes": "32", - "label": "_name", - "offset": 0, - "slot": "1", - "type": "string" - }, - { - "bytes": "32", - "label": "_symbol", - "offset": 0, - "slot": "2", - "type": "string" - }, - { - "bytes": "1", - "label": "_decimals", - "offset": 0, - "slot": "3", - "type": "uint8" - } -] \ No newline at end of file +[] \ No newline at end of file diff --git a/packages/contracts-bedrock/src/L2/IOptimismSuperchainERC20.sol b/packages/contracts-bedrock/src/L2/IOptimismSuperchainERC20.sol index 81a7f668d91e..5e86e21bbdae 100644 --- a/packages/contracts-bedrock/src/L2/IOptimismSuperchainERC20.sol +++ b/packages/contracts-bedrock/src/L2/IOptimismSuperchainERC20.sol @@ -52,6 +52,6 @@ interface IOptimismSuperchainERC20 { /// @param _amount Amount of tokens to relay. function relayERC20(address _from, address _to, uint256 _amount) external; - /// @notice Returns the address of the corresponding remote token. + /// @notice Returns the address of the corresponding version of this token on the remote chain. function remoteToken() external view returns (address); } diff --git a/packages/contracts-bedrock/src/L2/OptimismSuperchainERC20.sol b/packages/contracts-bedrock/src/L2/OptimismSuperchainERC20.sol index 5e8d155225b2..b2ff76ba8ac0 100644 --- a/packages/contracts-bedrock/src/L2/OptimismSuperchainERC20.sol +++ b/packages/contracts-bedrock/src/L2/OptimismSuperchainERC20.sol @@ -6,7 +6,7 @@ import { ERC20 } from "@solady/tokens/ERC20.sol"; import { IL2ToL2CrossDomainMessenger } from "src/L2/IL2ToL2CrossDomainMessenger.sol"; import { ISemver } from "src/universal/ISemver.sol"; import { Predeploys } from "src/libraries/Predeploys.sol"; -import { Initializable } from "@solady/utils/Initializable.sol"; +import { Initializable } from "@openzeppelin/contracts-v5/proxy/utils/Initializable.sol"; /// @notice Thrown when attempting to relay a message and the function caller (msg.sender) is not /// L2ToL2CrossDomainMessenger. @@ -37,17 +37,29 @@ contract OptimismSuperchainERC20 is IOptimismSuperchainERC20, ERC20, ISemver, In /// @notice Address of the StandardBridge Predeploy. address internal constant BRIDGE = Predeploys.L2_STANDARD_BRIDGE; - /// @notice Address of the corresponding version of this token on the remote chain. - address private _remoteToken; - - /// @notice Name of the token - string private _name; - - /// @notice Symbol of the token - string private _symbol; + /// @notice Storage slot that the OptimismSuperchainERC20Metadata struct is stored at. + bytes32 internal constant OPTIMISM_SUPERCHAIN_ERC20_METADATA_SLOT = + bytes32(uint256(keccak256("optimismSuperchainERC20Metadata")) - 1); + + /// @notice Storage struct for the OptimismSuperchainERC20 metadata. + struct OptimismSuperchainERC20Metadata { + /// @notice Address of the corresponding version of this token on the remote chain. + address remoteToken; + /// @notice Name of the token + string name; + /// @notice Symbol of the token + string symbol; + /// @notice Decimals of the token + uint8 decimals; + } - /// @notice Decimals of the token - uint8 private _decimals; + /// @notice Returns the storage for the OptimismSuperchainERC20Metadata. + function _getMetadataStorage() private pure returns (OptimismSuperchainERC20Metadata storage _storage) { + bytes32 _slot = OPTIMISM_SUPERCHAIN_ERC20_METADATA_SLOT; + assembly { + _storage.slot := _slot + } + } /// @notice A modifier that only allows the bridge to call modifier onlyBridge() { @@ -65,23 +77,24 @@ contract OptimismSuperchainERC20 is IOptimismSuperchainERC20, ERC20, ISemver, In } /// @notice Initializes the contract. - /// @param __remoteToken Address of the corresponding remote token. - /// @param __name ERC20 name. - /// @param __symbol ERC20 symbol. - /// @param __decimals ERC20 decimals. + /// @param _remoteToken Address of the corresponding remote token. + /// @param _name ERC20 name. + /// @param _symbol ERC20 symbol. + /// @param _decimals ERC20 decimals. function initialize( - address __remoteToken, - string memory __name, - string memory __symbol, - uint8 __decimals + address _remoteToken, + string memory _name, + string memory _symbol, + uint8 _decimals ) external initializer { - _remoteToken = __remoteToken; - _name = __name; - _symbol = __symbol; - _decimals = __decimals; + OptimismSuperchainERC20Metadata storage _storage = _getMetadataStorage(); + _storage.remoteToken = _remoteToken; + _storage.name = _name; + _storage.symbol = _symbol; + _storage.decimals = _decimals; } /// @notice Allows the L2StandardBridge to mint tokens. @@ -140,19 +153,19 @@ contract OptimismSuperchainERC20 is IOptimismSuperchainERC20, ERC20, ISemver, In emit RelayedERC20(_from, _to, _amount, _source); } - /// @notice Returns the address of the corresponding remote token. + /// @notice Returns the address of the corresponding version of this token on the remote chain. function remoteToken() public view override returns (address) { - return _remoteToken; + return _getMetadataStorage().remoteToken; } /// @notice Returns the name of the token. function name() public view virtual override returns (string memory) { - return _name; + return _getMetadataStorage().name; } /// @notice Returns the symbol of the token. function symbol() public view virtual override returns (string memory) { - return _symbol; + return _getMetadataStorage().symbol; } /// @notice Returns the number of decimals used to get its user representation. @@ -162,7 +175,7 @@ contract OptimismSuperchainERC20 is IOptimismSuperchainERC20, ERC20, ISemver, 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; + return _getMetadataStorage().decimals; } /// @notice ERC165 interface check function. diff --git a/packages/contracts-bedrock/test/L2/OptimismSuperchainERC20.t.sol b/packages/contracts-bedrock/test/L2/OptimismSuperchainERC20.t.sol index 0dfcafdf41eb..99afa44c1556 100644 --- a/packages/contracts-bedrock/test/L2/OptimismSuperchainERC20.t.sol +++ b/packages/contracts-bedrock/test/L2/OptimismSuperchainERC20.t.sol @@ -11,7 +11,7 @@ import { stdStorage, StdStorage } from "forge-std/Test.sol"; import { ERC20 } from "@openzeppelin/contracts/token/ERC20/ERC20.sol"; import { IL2ToL2CrossDomainMessenger } from "src/L2/IL2ToL2CrossDomainMessenger.sol"; import { ERC1967Proxy } from "@openzeppelin/contracts/proxy/ERC1967/ERC1967Proxy.sol"; -import { Initializable } from "@solady/utils/Initializable.sol"; +import { Initializable } from "@openzeppelin/contracts-v5/proxy/utils/Initializable.sol"; // Target contract import { diff --git a/packages/contracts-bedrock/test/vendor/Initializable.t.sol b/packages/contracts-bedrock/test/vendor/Initializable.t.sol index 05fff737bd6e..fa6af352ac99 100644 --- a/packages/contracts-bedrock/test/vendor/Initializable.t.sol +++ b/packages/contracts-bedrock/test/vendor/Initializable.t.sol @@ -352,7 +352,8 @@ contract Initializer_Test is Bridge_Initializer { // Ensure that all L1, L2 `Initializable` contracts are accounted for, in addition to // OptimismMintableERC20FactoryImpl, OptimismMintableERC20FactoryProxy, OptimismPortal2, // DisputeGameFactoryImpl, DisputeGameFactoryProxy, DelayedWETHImpl, DelayedWETHProxy. - assertEq(_getNumInitializable() + 1, contracts.length); + // Omitting OptimismSuperchainERC20 due to using OZ v5 Initializable. + assertEq(_getNumInitializable(), contracts.length); // Attempt to re-initialize all contracts within the `contracts` array. for (uint256 i; i < contracts.length; i++) { From 1ed58951ab7880df3cc9497d456759c1203e1f48 Mon Sep 17 00:00:00 2001 From: agusduha Date: Fri, 2 Aug 2024 14:37:43 -0300 Subject: [PATCH 12/18] feat: update superchain erc20 interfaces --- .../invariant-docs/SuperchainWETH.md | 2 +- packages/contracts-bedrock/semver-lock.json | 8 ++-- .../abi/OptimismSuperchainERC20.json | 2 +- .../snapshots/abi/SuperchainWETH.json | 29 +++++++++--- .../src/L2/IOptimismSuperchainERC20.sol | 37 ++++----------- .../src/L2/ISuperchainERC20.sol | 37 ++++++++------- .../src/L2/OptimismSuperchainERC20.sol | 14 +++--- .../src/L2/SuperchainWETH.sol | 9 ++-- .../test/L2/OptimismSuperchainERC20.t.sol | 19 ++++---- .../test/L2/SuperchainWETH.t.sol | 46 +++++++++++++------ .../test/invariants/SuperchainWETH.t.sol | 11 ++++- 11 files changed, 123 insertions(+), 91 deletions(-) diff --git a/packages/contracts-bedrock/invariant-docs/SuperchainWETH.md b/packages/contracts-bedrock/invariant-docs/SuperchainWETH.md index aaed4710b5dc..4514f20cb1b9 100644 --- a/packages/contracts-bedrock/invariant-docs/SuperchainWETH.md +++ b/packages/contracts-bedrock/invariant-docs/SuperchainWETH.md @@ -1,5 +1,5 @@ # `SuperchainWETH` Invariants ## Calls to sendERC20 should always succeed as long as the actor has less than uint248 wei which is much greater than the total ETH supply. Actor's balance should also not increase out of nowhere. -**Test:** [`SuperchainWETH.t.sol#L171`](../test/invariants/SuperchainWETH.t.sol#L171) +**Test:** [`SuperchainWETH.t.sol#L178`](../test/invariants/SuperchainWETH.t.sol#L178) diff --git a/packages/contracts-bedrock/semver-lock.json b/packages/contracts-bedrock/semver-lock.json index d556457ab8b9..f9d2656f3070 100644 --- a/packages/contracts-bedrock/semver-lock.json +++ b/packages/contracts-bedrock/semver-lock.json @@ -112,16 +112,16 @@ "sourceCodeHash": "0x7a9cddf5b54ac72457231f0c09b8e88398202ac29125cd63318b8389c81e119b" }, "src/L2/OptimismSuperchainERC20.sol": { - "initCodeHash": "0xbfa1d35644a8b97c657101289e64b92b8faeed0b0090195eb871da2de4583bb3", - "sourceCodeHash": "0x56bed1f02792ef9513fe209552e964c983a8f29dbcb2b8a76910fdae929f30ff" + "initCodeHash": "0x4f9b01e25c4ca9a275c2a2d6517e819d573cb6dae64cfe373496fb5e25c122cd", + "sourceCodeHash": "0x90fddf656378f9890141af03eaab9415c78ec292fec540c881e1bf912f0cee58" }, "src/L2/SequencerFeeVault.sol": { "initCodeHash": "0xb94145f571e92ee615c6fe903b6568e8aac5fe760b6b65148ffc45d2fb0f5433", "sourceCodeHash": "0x8f2a54104e5e7105ba03ba37e3ef9b6684a447245f0e0b787ba4cca12957b97c" }, "src/L2/SuperchainWETH.sol": { - "initCodeHash": "0x52e302ac749e6a519829e0fb01075638e481e7f010a6438088486a7a4be4601b", - "sourceCodeHash": "0x7c93752288f4414777e01c2962aee929a28aef2c1fccdfeba456f22df0f9aa39" + "initCodeHash": "0x599e948350c70d699f8a8be945abffd126097de97fade056d29767128320fe75", + "sourceCodeHash": "0x9690190b78baed4a692742531505aa582122f8ed6dbdc998baf973c3b5a43539" }, "src/L2/WETH.sol": { "initCodeHash": "0xde72ae96910e95249623c2d695749847e4c4adeaf96a7a35033afd77318a528a", diff --git a/packages/contracts-bedrock/snapshots/abi/OptimismSuperchainERC20.json b/packages/contracts-bedrock/snapshots/abi/OptimismSuperchainERC20.json index dd05c3f2bbf2..d7ebb6151307 100644 --- a/packages/contracts-bedrock/snapshots/abi/OptimismSuperchainERC20.json +++ b/packages/contracts-bedrock/snapshots/abi/OptimismSuperchainERC20.json @@ -510,7 +510,7 @@ "type": "uint256" } ], - "name": "RelayedERC20", + "name": "RelayERC20", "type": "event" }, { diff --git a/packages/contracts-bedrock/snapshots/abi/SuperchainWETH.json b/packages/contracts-bedrock/snapshots/abi/SuperchainWETH.json index 42d3a73e7c56..600e0e6b64f7 100644 --- a/packages/contracts-bedrock/snapshots/abi/SuperchainWETH.json +++ b/packages/contracts-bedrock/snapshots/abi/SuperchainWETH.json @@ -109,6 +109,11 @@ }, { "inputs": [ + { + "internalType": "address", + "name": "from", + "type": "address" + }, { "internalType": "address", "name": "dst", @@ -303,13 +308,25 @@ { "indexed": true, "internalType": "address", - "name": "_to", + "name": "from", + "type": "address" + }, + { + "indexed": true, + "internalType": "address", + "name": "to", "type": "address" }, { "indexed": false, "internalType": "uint256", - "name": "_amount", + "name": "amount", + "type": "uint256" + }, + { + "indexed": false, + "internalType": "uint256", + "name": "source", "type": "uint256" } ], @@ -322,25 +339,25 @@ { "indexed": true, "internalType": "address", - "name": "_from", + "name": "from", "type": "address" }, { "indexed": true, "internalType": "address", - "name": "_to", + "name": "to", "type": "address" }, { "indexed": false, "internalType": "uint256", - "name": "_amount", + "name": "amount", "type": "uint256" }, { "indexed": false, "internalType": "uint256", - "name": "_chainId", + "name": "destination", "type": "uint256" } ], diff --git a/packages/contracts-bedrock/src/L2/IOptimismSuperchainERC20.sol b/packages/contracts-bedrock/src/L2/IOptimismSuperchainERC20.sol index 5e86e21bbdae..9b9594e75d78 100644 --- a/packages/contracts-bedrock/src/L2/IOptimismSuperchainERC20.sol +++ b/packages/contracts-bedrock/src/L2/IOptimismSuperchainERC20.sol @@ -1,11 +1,14 @@ // SPDX-License-Identifier: MIT pragma solidity ^0.8.0; -/// @title IOptimismSuperchainERC20 +import { IERC20 } from "@openzeppelin/contracts/token/ERC20/IERC20.sol"; +import { ISuperchainERC20Extensions } from "./ISuperchainERC20.sol"; + +/// @title IOptimismSuperchainERC20Extension /// @notice This interface is available on the OptimismSuperchainERC20 contract. /// We declare it as a separate interface so that it can be used in /// custom implementations of SuperchainERC20. -interface IOptimismSuperchainERC20 { +interface IOptimismSuperchainERC20Extension is ISuperchainERC20Extensions { /// @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. @@ -16,20 +19,6 @@ interface IOptimismSuperchainERC20 { /// @param amount Amount of tokens burned. event Burn(address indexed account, uint256 amount); - /// @notice Emitted whenever tokens are sent to another chain. - /// @param from Address of the sender. - /// @param to Address of the recipient. - /// @param amount Amount of tokens sent. - /// @param destination Chain ID of the destination chain. - event SendERC20(address indexed from, address indexed to, uint256 amount, uint256 destination); - - /// @notice Emitted whenever tokens are successfully relayed on this chain. - /// @param from Address of the msg.sender of sendERC20 on the source chain. - /// @param to Address of the recipient. - /// @param amount Amount of tokens relayed. - /// @param source Chain ID of the source chain. - event RelayedERC20(address indexed from, address indexed to, uint256 amount, uint256 source); - /// @notice Allows the L2StandardBridge to mint tokens. /// @param _to Address to mint tokens to. /// @param _amount Amount of tokens to mint. @@ -40,18 +29,10 @@ interface IOptimismSuperchainERC20 { /// @param _amount Amount of tokens to burn. function burn(address _from, uint256 _amount) external; - /// @notice Sends tokens to some target address on another chain. - /// @param _to Address to send tokens to. - /// @param _amount Amount of tokens to send. - /// @param _chainId Chain ID of the destination chain. - function sendERC20(address _to, uint256 _amount, uint256 _chainId) external; - - /// @notice Relays tokens received from another chain. - /// @param _from Address of the msg.sender of sendERC20 on the source chain. - /// @param _to Address to relay tokens to. - /// @param _amount Amount of tokens to relay. - function relayERC20(address _from, address _to, uint256 _amount) external; - /// @notice Returns the address of the corresponding version of this token on the remote chain. function remoteToken() external view returns (address); } + +/// @title IOptimismSuperchainERC20 +/// @notice Combines the ERC20 interface with the OptimismSuperchainERC20Extension interface. +interface IOptimismSuperchainERC20 is IERC20, IOptimismSuperchainERC20Extension { } diff --git a/packages/contracts-bedrock/src/L2/ISuperchainERC20.sol b/packages/contracts-bedrock/src/L2/ISuperchainERC20.sol index b104a08d928e..76488cdf32ea 100644 --- a/packages/contracts-bedrock/src/L2/ISuperchainERC20.sol +++ b/packages/contracts-bedrock/src/L2/ISuperchainERC20.sol @@ -9,27 +9,30 @@ import { IERC20 } from "@openzeppelin/contracts/token/ERC20/IERC20.sol"; /// importing the full SuperchainERC20 interface would cause conflicting imports. interface ISuperchainERC20Extensions { /// @notice Emitted when tokens are sent from one chain to another. - /// @param _from Address of the sender. - /// @param _to Address of the recipient. - /// @param _amount Number of tokens sent. - /// @param _chainId Chain ID of the recipient. - event SendERC20(address indexed _from, address indexed _to, uint256 _amount, uint256 _chainId); + /// @param from Address of the sender. + /// @param to Address of the recipient. + /// @param amount Number of tokens sent. + /// @param destination Chain ID of the destination chain. + event SendERC20(address indexed from, address indexed to, uint256 amount, uint256 destination); - /// @notice Emitted when token sends are relayed to this chain. - /// @param _to Address of the recipient. - /// @param _amount Number of tokens sent. - event RelayERC20(address indexed _to, uint256 _amount); + /// @notice Emitted whenever tokens are successfully relayed on this chain. + /// @param from Address of the msg.sender of sendERC20 on the source chain. + /// @param to Address of the recipient. + /// @param amount Amount of tokens relayed. + /// @param source Chain ID of the source chain. + event RelayERC20(address indexed from, address indexed to, uint256 amount, uint256 source); - /// @notice Sends tokens to another chain. - /// @param _to Address of the recipient. - /// @param _amount Number of tokens to send. - /// @param _chainId Chain ID of the recipient. + /// @notice Sends tokens to some target address on another chain. + /// @param _to Address to send tokens to. + /// @param _amount Amount of tokens to send. + /// @param _chainId Chain ID of the destination chain. function sendERC20(address _to, uint256 _amount, uint256 _chainId) external; - /// @notice Relays a send of tokens to this chain. - /// @param _to Address of the recipient. - /// @param _amount Number of tokens sent. - function relayERC20(address _to, uint256 _amount) external; + /// @notice Relays tokens received from another chain. + /// @param _from Address of the msg.sender of sendERC20 on the source chain. + /// @param _to Address to relay tokens to. + /// @param _amount Amount of tokens to relay. + function relayERC20(address _from, address _to, uint256 _amount) external; } /// @title ISuperchainERC20 diff --git a/packages/contracts-bedrock/src/L2/OptimismSuperchainERC20.sol b/packages/contracts-bedrock/src/L2/OptimismSuperchainERC20.sol index b2ff76ba8ac0..83e250cddf0b 100644 --- a/packages/contracts-bedrock/src/L2/OptimismSuperchainERC20.sol +++ b/packages/contracts-bedrock/src/L2/OptimismSuperchainERC20.sol @@ -1,7 +1,7 @@ // SPDX-License-Identifier: MIT pragma solidity 0.8.25; -import { IOptimismSuperchainERC20 } from "src/L2/IOptimismSuperchainERC20.sol"; +import { IOptimismSuperchainERC20Extension } from "src/L2/IOptimismSuperchainERC20.sol"; import { ERC20 } from "@solady/tokens/ERC20.sol"; import { IL2ToL2CrossDomainMessenger } from "src/L2/IL2ToL2CrossDomainMessenger.sol"; import { ISemver } from "src/universal/ISemver.sol"; @@ -30,7 +30,7 @@ error ZeroAddress(); /// 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 IOptimismSuperchainERC20, ERC20, ISemver, Initializable { +contract OptimismSuperchainERC20 is IOptimismSuperchainERC20Extension, ERC20, ISemver, Initializable { /// @notice Address of the L2ToL2CrossDomainMessenger Predeploy. address internal constant MESSENGER = Predeploys.L2_TO_L2_CROSS_DOMAIN_MESSENGER; @@ -38,8 +38,9 @@ contract OptimismSuperchainERC20 is IOptimismSuperchainERC20, ERC20, ISemver, In address internal constant BRIDGE = Predeploys.L2_STANDARD_BRIDGE; /// @notice Storage slot that the OptimismSuperchainERC20Metadata struct is stored at. + /// Equal to bytes32(uint256(keccak256("optimismSuperchainERC20.metadata")) - 1) bytes32 internal constant OPTIMISM_SUPERCHAIN_ERC20_METADATA_SLOT = - bytes32(uint256(keccak256("optimismSuperchainERC20Metadata")) - 1); + 0x855c1a66176fd0f9748c66fe1bc8b9d3fecd35483489d9732ff7da2063f518b3; /// @notice Storage struct for the OptimismSuperchainERC20 metadata. struct OptimismSuperchainERC20Metadata { @@ -55,9 +56,8 @@ contract OptimismSuperchainERC20 is IOptimismSuperchainERC20, ERC20, ISemver, In /// @notice Returns the storage for the OptimismSuperchainERC20Metadata. function _getMetadataStorage() private pure returns (OptimismSuperchainERC20Metadata storage _storage) { - bytes32 _slot = OPTIMISM_SUPERCHAIN_ERC20_METADATA_SLOT; assembly { - _storage.slot := _slot + _storage.slot := OPTIMISM_SUPERCHAIN_ERC20_METADATA_SLOT } } @@ -150,7 +150,7 @@ contract OptimismSuperchainERC20 is IOptimismSuperchainERC20, ERC20, ISemver, In _mint(_to, _amount); - emit RelayedERC20(_from, _to, _amount, _source); + emit RelayERC20(_from, _to, _amount, _source); } /// @notice Returns the address of the corresponding version of this token on the remote chain. @@ -182,6 +182,6 @@ contract OptimismSuperchainERC20 is IOptimismSuperchainERC20, ERC20, ISemver, In /// @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) { - return _interfaceId == type(IOptimismSuperchainERC20).interfaceId; + return _interfaceId == type(IOptimismSuperchainERC20Extension).interfaceId; } } diff --git a/packages/contracts-bedrock/src/L2/SuperchainWETH.sol b/packages/contracts-bedrock/src/L2/SuperchainWETH.sol index 6e5a8c1fe4e1..555eb6cc1e7f 100644 --- a/packages/contracts-bedrock/src/L2/SuperchainWETH.sol +++ b/packages/contracts-bedrock/src/L2/SuperchainWETH.sol @@ -45,7 +45,7 @@ contract SuperchainWETH is WETH98, ISuperchainERC20Extensions, ISemver { IL2ToL2CrossDomainMessenger(Predeploys.L2_TO_L2_CROSS_DOMAIN_MESSENGER).sendMessage({ _destination: chainId, _target: address(this), - _message: abi.encodeCall(this.relayERC20, (dst, wad)) + _message: abi.encodeCall(this.relayERC20, (msg.sender, dst, wad)) }); // Emit event. @@ -53,7 +53,7 @@ contract SuperchainWETH is WETH98, ISuperchainERC20Extensions, ISemver { } /// @inheritdoc ISuperchainERC20Extensions - function relayERC20(address dst, uint256 wad) external { + function relayERC20(address from, address dst, uint256 wad) external { // Receive message from other chain. IL2ToL2CrossDomainMessenger messenger = IL2ToL2CrossDomainMessenger(Predeploys.L2_TO_L2_CROSS_DOMAIN_MESSENGER); if (msg.sender != address(messenger)) revert Unauthorized(); @@ -64,11 +64,14 @@ contract SuperchainWETH is WETH98, ISuperchainERC20Extensions, ISemver { ETHLiquidity(Predeploys.ETH_LIQUIDITY).mint(wad); } + // Get source chain ID. + uint256 _source = messenger.crossDomainMessageSource(); + // Mint to user's balance. _mint(dst, wad); // Emit event. - emit RelayERC20(dst, wad); + emit RelayERC20(from, dst, wad, _source); } /// @notice Mints WETH to an address. diff --git a/packages/contracts-bedrock/test/L2/OptimismSuperchainERC20.t.sol b/packages/contracts-bedrock/test/L2/OptimismSuperchainERC20.t.sol index 99afa44c1556..8e770977a0a3 100644 --- a/packages/contracts-bedrock/test/L2/OptimismSuperchainERC20.t.sol +++ b/packages/contracts-bedrock/test/L2/OptimismSuperchainERC20.t.sol @@ -16,12 +16,13 @@ import { Initializable } from "@openzeppelin/contracts-v5/proxy/utils/Initializa // Target contract import { OptimismSuperchainERC20, - IOptimismSuperchainERC20, + IOptimismSuperchainERC20Extension, CallerNotL2ToL2CrossDomainMessenger, InvalidCrossDomainSender, OnlyBridge, ZeroAddress } from "src/L2/OptimismSuperchainERC20.sol"; +import { ISuperchainERC20Extensions } from "src/L2/ISuperchainERC20.sol"; /// @title OptimismSuperchainERC20Test /// @notice Contract for testing the OptimismSuperchainERC20 contract. @@ -131,7 +132,7 @@ contract OptimismSuperchainERC20Test is Test { // Look for the emit of the `Mint` event vm.expectEmit(true, true, true, true, address(superchainERC20)); - emit IOptimismSuperchainERC20.Mint(_to, _amount); + emit IOptimismSuperchainERC20Extension.Mint(_to, _amount); // Call the `mint` function with the bridge caller vm.prank(BRIDGE); @@ -184,7 +185,7 @@ contract OptimismSuperchainERC20Test is Test { // Look for the emit of the `Burn` event vm.expectEmit(true, true, true, true, address(superchainERC20)); - emit IOptimismSuperchainERC20.Burn(_from, _amount); + emit IOptimismSuperchainERC20Extension.Burn(_from, _amount); // Call the `burn` function with the bridge caller vm.prank(BRIDGE); @@ -226,7 +227,7 @@ contract OptimismSuperchainERC20Test is Test { // Look for the emit of the `SendERC20` event vm.expectEmit(true, true, true, true, address(superchainERC20)); - emit IOptimismSuperchainERC20.SendERC20(_sender, _to, _amount, _chainId); + emit ISuperchainERC20Extensions.SendERC20(_sender, _to, _amount, _chainId); // Mock the call over the `sendMessage` function and expect it to be called properly bytes memory _message = abi.encodeCall(superchainERC20.relayERC20, (_sender, _to, _amount)); @@ -305,7 +306,7 @@ contract OptimismSuperchainERC20Test is Test { superchainERC20.relayERC20({ _from: ZERO_ADDRESS, _to: ZERO_ADDRESS, _amount: _amount }); } - /// @notice Tests the `relayERC20` mints the proper amount and emits the `RelayedERC20` event. + /// @notice Tests the `relayERC20` mints the proper amount and emits the `RelayERC20` event. function testFuzz_relayERC20_succeeds(address _from, address _to, uint256 _amount, uint256 _source) public { vm.assume(_from != ZERO_ADDRESS); vm.assume(_to != ZERO_ADDRESS); @@ -332,9 +333,9 @@ contract OptimismSuperchainERC20Test is Test { vm.expectEmit(true, true, true, true, address(superchainERC20)); emit IERC20.Transfer(ZERO_ADDRESS, _to, _amount); - // Look for the emit of the `RelayedERC20` event + // Look for the emit of the `RelayERC20` event vm.expectEmit(true, true, true, true, address(superchainERC20)); - emit IOptimismSuperchainERC20.RelayedERC20(_from, _to, _amount, _source); + emit ISuperchainERC20Extensions.RelayERC20(_from, _to, _amount, _source); // Call the `relayERC20` function with the messenger caller vm.prank(MESSENGER); @@ -371,13 +372,13 @@ contract OptimismSuperchainERC20Test is Test { /// @notice Tests that the `supportsInterface` function returns true for the `IOptimismSuperchainERC20` interface. function test_supportInterface_succeeds() public view { - assertTrue(superchainERC20.supportsInterface(type(IOptimismSuperchainERC20).interfaceId)); + assertTrue(superchainERC20.supportsInterface(type(IOptimismSuperchainERC20Extension).interfaceId)); } /// @notice Tests that the `supportsInterface` function returns false for any other interface than the /// `IOptimismSuperchainERC20` one. function testFuzz_supportInterface_returnFalse(bytes4 _interfaceId) public view { - vm.assume(_interfaceId != type(IOptimismSuperchainERC20).interfaceId); + vm.assume(_interfaceId != type(IOptimismSuperchainERC20Extension).interfaceId); assertFalse(superchainERC20.supportsInterface(_interfaceId)); } } diff --git a/packages/contracts-bedrock/test/L2/SuperchainWETH.t.sol b/packages/contracts-bedrock/test/L2/SuperchainWETH.t.sol index 2024f8ddbb40..72cc6b084a2b 100644 --- a/packages/contracts-bedrock/test/L2/SuperchainWETH.t.sol +++ b/packages/contracts-bedrock/test/L2/SuperchainWETH.t.sol @@ -26,7 +26,7 @@ contract SuperchainWETH_Test is CommonTest { event SendERC20(address indexed _from, address indexed _to, uint256 _amount, uint256 _chainId); /// @notice Emitted when an ERC20 send is relayed. - event RelayERC20(address indexed _to, uint256 _amount); + event RelayERC20(address indexed _from, address indexed _to, uint256 _amount, uint256 _source); /// @notice Test setup. function setUp() public virtual override { @@ -150,7 +150,11 @@ contract SuperchainWETH_Test is CommonTest { Predeploys.L2_TO_L2_CROSS_DOMAIN_MESSENGER, abi.encodeCall( IL2ToL2CrossDomainMessenger.sendMessage, - (_chainId, address(superchainWeth), abi.encodeCall(superchainWeth.relayERC20, (_recipient, _amount))) + ( + _chainId, + address(superchainWeth), + abi.encodeCall(superchainWeth.relayERC20, (_caller, _recipient, _amount)) + ) ), 1 ); @@ -187,7 +191,7 @@ contract SuperchainWETH_Test is CommonTest { Predeploys.L2_TO_L2_CROSS_DOMAIN_MESSENGER, abi.encodeCall( IL2ToL2CrossDomainMessenger.sendMessage, - (_chainId, address(superchainWeth), abi.encodeCall(superchainWeth.relayERC20, (bob, _amount))) + (_chainId, address(superchainWeth), abi.encodeCall(superchainWeth.relayERC20, (alice, bob, _amount))) ), 1 ); @@ -224,7 +228,7 @@ contract SuperchainWETH_Test is CommonTest { /// L2ToL2CrossDomainMessenger as long as the crossDomainMessageSender is the /// SuperchainWETH contract. /// @param _amount The amount of WETH to send. - function testFuzz_relayERC20_fromMessenger_succeeds(uint256 _amount) public { + function testFuzz_relayERC20_fromMessenger_succeeds(address _sender, uint256 _amount, uint256 _chainId) public { // Assume _amount = bound(_amount, 0, type(uint248).max - 1); @@ -234,13 +238,18 @@ contract SuperchainWETH_Test is CommonTest { abi.encodeCall(IL2ToL2CrossDomainMessenger.crossDomainMessageSender, ()), abi.encode(address(superchainWeth)) ); + vm.mockCall( + Predeploys.L2_TO_L2_CROSS_DOMAIN_MESSENGER, + abi.encodeCall(IL2ToL2CrossDomainMessenger.crossDomainMessageSource, ()), + abi.encode(_chainId) + ); // Act vm.expectEmit(address(superchainWeth)); - emit RelayERC20(bob, _amount); + emit RelayERC20(_sender, bob, _amount, _chainId); vm.expectCall(Predeploys.ETH_LIQUIDITY, abi.encodeCall(ETHLiquidity.mint, (_amount)), 1); vm.prank(Predeploys.L2_TO_L2_CROSS_DOMAIN_MESSENGER); - superchainWeth.relayERC20(bob, _amount); + superchainWeth.relayERC20(_sender, bob, _amount); // Assert assertEq(address(superchainWeth).balance, _amount); @@ -252,7 +261,13 @@ contract SuperchainWETH_Test is CommonTest { /// SuperchainWETH contract, even when the chain is a custom gas token chain. Shows /// that ETH is not minted in this case but the SuperchainWETH balance is updated. /// @param _amount The amount of WETH to send. - function testFuzz_relayERC20_fromMessengerCustomGasTokenChain_succeeds(uint256 _amount) public { + function testFuzz_relayERC20_fromMessengerCustomGasTokenChain_succeeds( + address _sender, + uint256 _amount, + uint256 _chainId + ) + public + { // Assume _amount = bound(_amount, 0, type(uint248).max - 1); @@ -262,14 +277,19 @@ contract SuperchainWETH_Test is CommonTest { abi.encodeCall(IL2ToL2CrossDomainMessenger.crossDomainMessageSender, ()), abi.encode(address(superchainWeth)) ); + vm.mockCall( + Predeploys.L2_TO_L2_CROSS_DOMAIN_MESSENGER, + abi.encodeCall(IL2ToL2CrossDomainMessenger.crossDomainMessageSource, ()), + abi.encode(_chainId) + ); vm.mockCall(address(l1Block), abi.encodeCall(l1Block.isCustomGasToken, ()), abi.encode(true)); // Act vm.expectEmit(address(superchainWeth)); - emit RelayERC20(bob, _amount); + emit RelayERC20(_sender, bob, _amount, _chainId); vm.expectCall(Predeploys.ETH_LIQUIDITY, abi.encodeCall(ETHLiquidity.mint, (_amount)), 0); vm.prank(Predeploys.L2_TO_L2_CROSS_DOMAIN_MESSENGER); - superchainWeth.relayERC20(bob, _amount); + superchainWeth.relayERC20(_sender, bob, _amount); // Assert assertEq(address(superchainWeth).balance, 0); @@ -279,7 +299,7 @@ contract SuperchainWETH_Test is CommonTest { /// @notice Tests that the relayERC20 function reverts when not called from the /// L2ToL2CrossDomainMessenger. /// @param _amount The amount of WETH to send. - function testFuzz_relayERC20_notFromMessenger_fails(uint256 _amount) public { + function testFuzz_relayERC20_notFromMessenger_fails(address _sender, uint256 _amount) public { // Assume _amount = bound(_amount, 0, type(uint248).max - 1); @@ -289,7 +309,7 @@ contract SuperchainWETH_Test is CommonTest { // Act vm.expectRevert(Unauthorized.selector); vm.prank(alice); - superchainWeth.relayERC20(bob, _amount); + superchainWeth.relayERC20(_sender, bob, _amount); // Assert assertEq(address(superchainWeth).balance, 0); @@ -300,7 +320,7 @@ contract SuperchainWETH_Test is CommonTest { /// L2ToL2CrossDomainMessenger but the crossDomainMessageSender is not the /// SuperchainWETH contract. /// @param _amount The amount of WETH to send. - function testFuzz_relayERC20_fromMessengerNotFromSuperchainWETH_fails(uint256 _amount) public { + function testFuzz_relayERC20_fromMessengerNotFromSuperchainWETH_fails(address _sender, uint256 _amount) public { // Assume _amount = bound(_amount, 0, type(uint248).max - 1); @@ -314,7 +334,7 @@ contract SuperchainWETH_Test is CommonTest { // Act vm.expectRevert(Unauthorized.selector); vm.prank(Predeploys.L2_TO_L2_CROSS_DOMAIN_MESSENGER); - superchainWeth.relayERC20(bob, _amount); + superchainWeth.relayERC20(_sender, bob, _amount); // Assert assertEq(address(superchainWeth).balance, 0); diff --git a/packages/contracts-bedrock/test/invariants/SuperchainWETH.t.sol b/packages/contracts-bedrock/test/invariants/SuperchainWETH.t.sol index 19318fbda6e1..377eda410992 100644 --- a/packages/contracts-bedrock/test/invariants/SuperchainWETH.t.sol +++ b/packages/contracts-bedrock/test/invariants/SuperchainWETH.t.sol @@ -100,7 +100,7 @@ contract SuperchainWETH_User is StdUtils { } /// @notice Relay a message from another chain. - function relayMessage() public { + function relayMessage(uint256 _source) public { // Make sure there are unrelayed messages. if (unrelayed.length == 0) { return; @@ -117,10 +117,17 @@ contract SuperchainWETH_User is StdUtils { abi.encode(address(weth)) ); + // Simulate the cross-domain message source to any chain. + vm.mockCall( + Predeploys.L2_TO_L2_CROSS_DOMAIN_MESSENGER, + abi.encodeCall(IL2ToL2CrossDomainMessenger.crossDomainMessageSource, ()), + abi.encode(_source) + ); + // Prank the relayERC20 function. // Balance will just go back to our own account. vm.prank(Predeploys.L2_TO_L2_CROSS_DOMAIN_MESSENGER); - try weth.relayERC20(address(this), message.amount) { + try weth.relayERC20(address(this), address(this), message.amount) { // Success. } catch { failed = true; From 8de4f012b593bfc32f242551a1e89678dc89b132 Mon Sep 17 00:00:00 2001 From: agusduha Date: Tue, 6 Aug 2024 14:01:10 -0300 Subject: [PATCH 13/18] fix: adapt storage to ERC7201 --- packages/contracts-bedrock/semver-lock.json | 6 +++--- .../src/L2/OptimismSuperchainERC20.sol | 10 ++++++---- packages/contracts-bedrock/src/L2/SuperchainWETH.sol | 4 ++-- .../test/L2/OptimismSuperchainERC20.t.sol | 1 + 4 files changed, 12 insertions(+), 9 deletions(-) diff --git a/packages/contracts-bedrock/semver-lock.json b/packages/contracts-bedrock/semver-lock.json index f9d2656f3070..28ea624b1422 100644 --- a/packages/contracts-bedrock/semver-lock.json +++ b/packages/contracts-bedrock/semver-lock.json @@ -112,8 +112,8 @@ "sourceCodeHash": "0x7a9cddf5b54ac72457231f0c09b8e88398202ac29125cd63318b8389c81e119b" }, "src/L2/OptimismSuperchainERC20.sol": { - "initCodeHash": "0x4f9b01e25c4ca9a275c2a2d6517e819d573cb6dae64cfe373496fb5e25c122cd", - "sourceCodeHash": "0x90fddf656378f9890141af03eaab9415c78ec292fec540c881e1bf912f0cee58" + "initCodeHash": "0xda1049fbca7fb97740c3ed9569ab5a4720872d1c5cf06e47eb38eb62b0f3e5eb", + "sourceCodeHash": "0x02273e470de25d25b9b3025550b32c7f2e949a4e986c0f136cf70996eae3ca90" }, "src/L2/SequencerFeeVault.sol": { "initCodeHash": "0xb94145f571e92ee615c6fe903b6568e8aac5fe760b6b65148ffc45d2fb0f5433", @@ -121,7 +121,7 @@ }, "src/L2/SuperchainWETH.sol": { "initCodeHash": "0x599e948350c70d699f8a8be945abffd126097de97fade056d29767128320fe75", - "sourceCodeHash": "0x9690190b78baed4a692742531505aa582122f8ed6dbdc998baf973c3b5a43539" + "sourceCodeHash": "0x3df29ee1321418914d88ce303b521bf8267ef234b919870b26639d08d7f806bd" }, "src/L2/WETH.sol": { "initCodeHash": "0xde72ae96910e95249623c2d695749847e4c4adeaf96a7a35033afd77318a528a", diff --git a/packages/contracts-bedrock/src/L2/OptimismSuperchainERC20.sol b/packages/contracts-bedrock/src/L2/OptimismSuperchainERC20.sol index 83e250cddf0b..09186bd6805b 100644 --- a/packages/contracts-bedrock/src/L2/OptimismSuperchainERC20.sol +++ b/packages/contracts-bedrock/src/L2/OptimismSuperchainERC20.sol @@ -38,11 +38,12 @@ contract OptimismSuperchainERC20 is IOptimismSuperchainERC20Extension, ERC20, IS address internal constant BRIDGE = Predeploys.L2_STANDARD_BRIDGE; /// @notice Storage slot that the OptimismSuperchainERC20Metadata struct is stored at. - /// Equal to bytes32(uint256(keccak256("optimismSuperchainERC20.metadata")) - 1) + /// keccak256(abi.encode(uint256(keccak256("optimismSuperchainERC20.metadata")) - 1)) & ~bytes32(uint256(0xff)); bytes32 internal constant OPTIMISM_SUPERCHAIN_ERC20_METADATA_SLOT = - 0x855c1a66176fd0f9748c66fe1bc8b9d3fecd35483489d9732ff7da2063f518b3; + 0x07f04e84143df95a6373fcf376312ae41da81a193a3089073a54f47a74d8fb00; /// @notice Storage struct for the OptimismSuperchainERC20 metadata. + /// @custom:storage-location erc7201:optimismSuperchainERC20.metadata struct OptimismSuperchainERC20Metadata { /// @notice Address of the corresponding version of this token on the remote chain. address remoteToken; @@ -146,11 +147,12 @@ contract OptimismSuperchainERC20 is IOptimismSuperchainERC20Extension, ERC20, IS if (IL2ToL2CrossDomainMessenger(MESSENGER).crossDomainMessageSender() != address(this)) { revert InvalidCrossDomainSender(); } - uint256 _source = IL2ToL2CrossDomainMessenger(MESSENGER).crossDomainMessageSource(); + + uint256 source = IL2ToL2CrossDomainMessenger(MESSENGER).crossDomainMessageSource(); _mint(_to, _amount); - emit RelayERC20(_from, _to, _amount, _source); + emit RelayERC20(_from, _to, _amount, source); } /// @notice Returns the address of the corresponding version of this token on the remote chain. diff --git a/packages/contracts-bedrock/src/L2/SuperchainWETH.sol b/packages/contracts-bedrock/src/L2/SuperchainWETH.sol index 555eb6cc1e7f..7080460254fc 100644 --- a/packages/contracts-bedrock/src/L2/SuperchainWETH.sol +++ b/packages/contracts-bedrock/src/L2/SuperchainWETH.sol @@ -65,13 +65,13 @@ contract SuperchainWETH is WETH98, ISuperchainERC20Extensions, ISemver { } // Get source chain ID. - uint256 _source = messenger.crossDomainMessageSource(); + uint256 source = messenger.crossDomainMessageSource(); // Mint to user's balance. _mint(dst, wad); // Emit event. - emit RelayERC20(from, dst, wad, _source); + emit RelayERC20(from, dst, wad, source); } /// @notice Mints WETH to an address. diff --git a/packages/contracts-bedrock/test/L2/OptimismSuperchainERC20.t.sol b/packages/contracts-bedrock/test/L2/OptimismSuperchainERC20.t.sol index 8e770977a0a3..15c0d1771690 100644 --- a/packages/contracts-bedrock/test/L2/OptimismSuperchainERC20.t.sol +++ b/packages/contracts-bedrock/test/L2/OptimismSuperchainERC20.t.sol @@ -56,6 +56,7 @@ contract OptimismSuperchainERC20Test is Test { { return OptimismSuperchainERC20( address( + // TODO: Use the SuperchainERC20 Beacon Proxy new ERC1967Proxy( address(superchainERC20Impl), abi.encodeCall(OptimismSuperchainERC20.initialize, (_remoteToken, _name, _symbol, _decimals)) From d167dce43ae9efdfa90f32356d692cfa0c02d87e Mon Sep 17 00:00:00 2001 From: agusduha Date: Tue, 6 Aug 2024 14:01:40 -0300 Subject: [PATCH 14/18] test: add initializable OZ v5 test --- .../test/vendor/InitializableOZv5.t.sol | 66 +++++++++++++++++++ 1 file changed, 66 insertions(+) create mode 100644 packages/contracts-bedrock/test/vendor/InitializableOZv5.t.sol diff --git a/packages/contracts-bedrock/test/vendor/InitializableOZv5.t.sol b/packages/contracts-bedrock/test/vendor/InitializableOZv5.t.sol new file mode 100644 index 000000000000..0820f987414a --- /dev/null +++ b/packages/contracts-bedrock/test/vendor/InitializableOZv5.t.sol @@ -0,0 +1,66 @@ +// SPDX-License-Identifier: MIT +pragma solidity 0.8.25; + +import { Test } from "forge-std/Test.sol"; +import { OptimismSuperchainERC20 } from "src/L2/OptimismSuperchainERC20.sol"; +import { Initializable } from "@openzeppelin/contracts-v5/proxy/utils/Initializable.sol"; + +/// @title InitializerOZv5_Test +/// @dev Ensures that the `initialize()` function on contracts cannot be called more than +/// once. Tests the contracts inheriting from `Initializable` from OpenZeppelin Contracts v5. +contract InitializerOZv5_Test is Test { + /// @notice The storage slot of the `initialized` flag in the `Initializable` contract from OZ v5. + /// keccak256(abi.encode(uint256(keccak256("openzeppelin.storage.Initializable")) - 1)) & ~bytes32(uint256(0xff)) + bytes32 private constant INITIALIZABLE_STORAGE = 0xf0c57e16840df040f15088dc2f81fe391c3923bec73e23a9662efc9c229c6a00; + + /// @notice Contains the address of an `Initializable` contract and the calldata + /// used to initialize it. + struct InitializeableContract { + address target; + bytes initCalldata; + } + + /// @notice Contains the addresses of the contracts to test as well as the calldata + /// used to initialize them. + InitializeableContract[] contracts; + + function setUp() public { + // Initialize the `contracts` array with the addresses of the contracts to test and the + // calldata used to initialize them + + // OptimismSuperchainERC20 + contracts.push( + InitializeableContract({ + target: address(new OptimismSuperchainERC20()), + initCalldata: abi.encodeCall(OptimismSuperchainERC20.initialize, (address(0), "", "", 18)) + }) + ); + } + + /// @notice Tests that: + /// 1. The `initialized` flag of each contract is properly set to `type(uint64).max`, + /// signifying that the contracts are initialized. + /// 2. The `initialize()` function of each contract cannot be called more than once. + /// 3. Returns the correct error when attempting to re-initialize a contract. + function test_cannotReinitialize_succeeds() public { + // Attempt to re-initialize all contracts within the `contracts` array. + for (uint256 i; i < contracts.length; i++) { + InitializeableContract memory _contract = contracts[i]; + uint256 size; + address target = _contract.target; + assembly { + size := extcodesize(target) + } + + // Assert that the contract is already initialized. + bytes32 slotVal = vm.load(_contract.target, INITIALIZABLE_STORAGE); + uint64 initialized = uint64(uint256(slotVal)); + assertEq(initialized, type(uint64).max); + + // Then, attempt to re-initialize the contract. This should fail. + (bool success, bytes memory returnData) = _contract.target.call(_contract.initCalldata); + assertFalse(success); + assertEq(bytes4(returnData), Initializable.InvalidInitialization.selector); + } + } +} From e0e59101761b9acf2bcb19736be54c5d5ce66c48 Mon Sep 17 00:00:00 2001 From: agusduha Date: Tue, 6 Aug 2024 14:21:27 -0300 Subject: [PATCH 15/18] fix: invariant docs --- packages/contracts-bedrock/invariant-docs/SuperchainWETH.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/contracts-bedrock/invariant-docs/SuperchainWETH.md b/packages/contracts-bedrock/invariant-docs/SuperchainWETH.md index 4d6f03724e74..4b52a6146f14 100644 --- a/packages/contracts-bedrock/invariant-docs/SuperchainWETH.md +++ b/packages/contracts-bedrock/invariant-docs/SuperchainWETH.md @@ -1,5 +1,5 @@ # `SuperchainWETH` Invariants ## Calls to sendERC20 should always succeed as long as the actor has less than uint248 wei which is much greater than the total ETH supply. Actor's balance should also not increase out of nowhere. +**Test:** [`SuperchainWETH.t.sol#L181`](../test/invariants/SuperchainWETH.t.sol#L181) -**Test:** [`SuperchainWETH.t.sol#L174`](../test/invariants/SuperchainWETH.t.sol#L174) From 55146943bb2c9665ed66019b7a49013a79cb3567 Mon Sep 17 00:00:00 2001 From: agusduha Date: Tue, 6 Aug 2024 14:55:44 -0300 Subject: [PATCH 16/18] fix: ERC165 implementation --- packages/contracts-bedrock/.gas-snapshot | 8 ++++---- packages/contracts-bedrock/semver-lock.json | 4 ++-- .../snapshots/abi/OptimismSuperchainERC20.json | 2 +- .../contracts-bedrock/src/L2/OptimismSuperchainERC20.sol | 8 +++++--- .../test/L2/OptimismSuperchainERC20.t.sol | 9 +++++---- 5 files changed, 17 insertions(+), 14 deletions(-) diff --git a/packages/contracts-bedrock/.gas-snapshot b/packages/contracts-bedrock/.gas-snapshot index b3ea3b88545e..a97f05678b58 100644 --- a/packages/contracts-bedrock/.gas-snapshot +++ b/packages/contracts-bedrock/.gas-snapshot @@ -1,7 +1,7 @@ -GasBenchMark_L1CrossDomainMessenger:test_sendMessage_benchmark_0() (gas: 369356) -GasBenchMark_L1CrossDomainMessenger:test_sendMessage_benchmark_1() (gas: 2967496) -GasBenchMark_L1StandardBridge_Deposit:test_depositERC20_benchmark_0() (gas: 564483) -GasBenchMark_L1StandardBridge_Deposit:test_depositERC20_benchmark_1() (gas: 4076526) +GasBenchMark_L1CrossDomainMessenger:test_sendMessage_benchmark_0() (gas: 369380) +GasBenchMark_L1CrossDomainMessenger:test_sendMessage_benchmark_1() (gas: 2967520) +GasBenchMark_L1StandardBridge_Deposit:test_depositERC20_benchmark_0() (gas: 561992) +GasBenchMark_L1StandardBridge_Deposit:test_depositERC20_benchmark_1() (gas: 4074035) GasBenchMark_L1StandardBridge_Deposit:test_depositETH_benchmark_0() (gas: 466947) GasBenchMark_L1StandardBridge_Deposit:test_depositETH_benchmark_1() (gas: 3512629) GasBenchMark_L1StandardBridge_Finalize:test_finalizeETHWithdrawal_benchmark() (gas: 72624) diff --git a/packages/contracts-bedrock/semver-lock.json b/packages/contracts-bedrock/semver-lock.json index 28ea624b1422..38d740020b86 100644 --- a/packages/contracts-bedrock/semver-lock.json +++ b/packages/contracts-bedrock/semver-lock.json @@ -112,8 +112,8 @@ "sourceCodeHash": "0x7a9cddf5b54ac72457231f0c09b8e88398202ac29125cd63318b8389c81e119b" }, "src/L2/OptimismSuperchainERC20.sol": { - "initCodeHash": "0xda1049fbca7fb97740c3ed9569ab5a4720872d1c5cf06e47eb38eb62b0f3e5eb", - "sourceCodeHash": "0x02273e470de25d25b9b3025550b32c7f2e949a4e986c0f136cf70996eae3ca90" + "initCodeHash": "0xd49214518ea1a30a43fac09f28b2cee9be570894a500cef342762c9820a070b0", + "sourceCodeHash": "0x6943d40010dcbd1d51dc3668d0a154fbb1568ea49ebcf3aa039d65ef6eab321b" }, "src/L2/SequencerFeeVault.sol": { "initCodeHash": "0xb94145f571e92ee615c6fe903b6568e8aac5fe760b6b65148ffc45d2fb0f5433", diff --git a/packages/contracts-bedrock/snapshots/abi/OptimismSuperchainERC20.json b/packages/contracts-bedrock/snapshots/abi/OptimismSuperchainERC20.json index d7ebb6151307..6eb57764a8cb 100644 --- a/packages/contracts-bedrock/snapshots/abi/OptimismSuperchainERC20.json +++ b/packages/contracts-bedrock/snapshots/abi/OptimismSuperchainERC20.json @@ -311,7 +311,7 @@ "type": "bool" } ], - "stateMutability": "pure", + "stateMutability": "view", "type": "function" }, { diff --git a/packages/contracts-bedrock/src/L2/OptimismSuperchainERC20.sol b/packages/contracts-bedrock/src/L2/OptimismSuperchainERC20.sol index 09186bd6805b..9b0ba5cad8b0 100644 --- a/packages/contracts-bedrock/src/L2/OptimismSuperchainERC20.sol +++ b/packages/contracts-bedrock/src/L2/OptimismSuperchainERC20.sol @@ -7,6 +7,7 @@ import { IL2ToL2CrossDomainMessenger } from "src/L2/IL2ToL2CrossDomainMessenger. import { ISemver } from "src/universal/ISemver.sol"; import { Predeploys } from "src/libraries/Predeploys.sol"; import { Initializable } from "@openzeppelin/contracts-v5/proxy/utils/Initializable.sol"; +import { ERC165 } from "@openzeppelin/contracts-v5/utils/introspection/ERC165.sol"; /// @notice Thrown when attempting to relay a message and the function caller (msg.sender) is not /// L2ToL2CrossDomainMessenger. @@ -30,7 +31,7 @@ error ZeroAddress(); /// 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 IOptimismSuperchainERC20Extension, ERC20, ISemver, Initializable { +contract OptimismSuperchainERC20 is IOptimismSuperchainERC20Extension, ERC20, ISemver, Initializable, ERC165 { /// @notice Address of the L2ToL2CrossDomainMessenger Predeploy. address internal constant MESSENGER = Predeploys.L2_TO_L2_CROSS_DOMAIN_MESSENGER; @@ -183,7 +184,8 @@ contract OptimismSuperchainERC20 is IOptimismSuperchainERC20Extension, ERC20, IS /// @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) { - return _interfaceId == type(IOptimismSuperchainERC20Extension).interfaceId; + function supportsInterface(bytes4 _interfaceId) public view virtual override returns (bool) { + return + _interfaceId == type(IOptimismSuperchainERC20Extension).interfaceId || super.supportsInterface(_interfaceId); } } diff --git a/packages/contracts-bedrock/test/L2/OptimismSuperchainERC20.t.sol b/packages/contracts-bedrock/test/L2/OptimismSuperchainERC20.t.sol index 15c0d1771690..84580fdd8687 100644 --- a/packages/contracts-bedrock/test/L2/OptimismSuperchainERC20.t.sol +++ b/packages/contracts-bedrock/test/L2/OptimismSuperchainERC20.t.sol @@ -6,12 +6,11 @@ import { Test } from "forge-std/Test.sol"; // Libraries import { Predeploys } from "src/libraries/Predeploys.sol"; -import { IERC20 } from "@openzeppelin/contracts/token/ERC20/IERC20.sol"; -import { stdStorage, StdStorage } from "forge-std/Test.sol"; -import { ERC20 } from "@openzeppelin/contracts/token/ERC20/ERC20.sol"; +import { IERC20 } from "@openzeppelin/contracts-v5/token/ERC20/IERC20.sol"; import { IL2ToL2CrossDomainMessenger } from "src/L2/IL2ToL2CrossDomainMessenger.sol"; -import { ERC1967Proxy } from "@openzeppelin/contracts/proxy/ERC1967/ERC1967Proxy.sol"; +import { ERC1967Proxy } from "@openzeppelin/contracts-v5/proxy/ERC1967/ERC1967Proxy.sol"; import { Initializable } from "@openzeppelin/contracts-v5/proxy/utils/Initializable.sol"; +import { IERC165 } from "@openzeppelin/contracts-v5/utils/introspection/IERC165.sol"; // Target contract import { @@ -373,12 +372,14 @@ contract OptimismSuperchainERC20Test is Test { /// @notice Tests that the `supportsInterface` function returns true for the `IOptimismSuperchainERC20` interface. function test_supportInterface_succeeds() public view { + assertTrue(superchainERC20.supportsInterface(type(IERC165).interfaceId)); assertTrue(superchainERC20.supportsInterface(type(IOptimismSuperchainERC20Extension).interfaceId)); } /// @notice Tests that the `supportsInterface` function returns false for any other interface than the /// `IOptimismSuperchainERC20` one. function testFuzz_supportInterface_returnFalse(bytes4 _interfaceId) public view { + vm.assume(_interfaceId != type(IERC165).interfaceId); vm.assume(_interfaceId != type(IOptimismSuperchainERC20Extension).interfaceId); assertFalse(superchainERC20.supportsInterface(_interfaceId)); } From eac988423cc6750b64404c01ee539e107fe9ea4c Mon Sep 17 00:00:00 2001 From: Disco <131301107+0xDiscotech@users.noreply.github.com> Date: Thu, 8 Aug 2024 12:15:08 -0300 Subject: [PATCH 17/18] test: improve superc20 invariant (#11) --- .../invariant-docs/OptimismSuperchainERC20.md | 9 ++- .../invariants/OptimismSuperchainERC20.t.sol | 65 +++++++++++++++---- 2 files changed, 60 insertions(+), 14 deletions(-) diff --git a/packages/contracts-bedrock/invariant-docs/OptimismSuperchainERC20.md b/packages/contracts-bedrock/invariant-docs/OptimismSuperchainERC20.md index 6d2904fd3e75..0e3150624da5 100644 --- a/packages/contracts-bedrock/invariant-docs/OptimismSuperchainERC20.md +++ b/packages/contracts-bedrock/invariant-docs/OptimismSuperchainERC20.md @@ -1,5 +1,10 @@ # `OptimismSuperchainERC20` Invariants -## Calls to sendERC20 should always succeed as long as the actor has enough balance. Actor's balance should also not increase out of nowhere. -**Test:** [`OptimismSuperchainERC20.t.sol#L177`](../test/invariants/OptimismSuperchainERC20.t.sol#L177) +## Calls to sendERC20 should always succeed as long as the actor has enough balance. Actor's balance should also not increase out of nowhere but instead should decrease by the amount sent. +**Test:** [`OptimismSuperchainERC20.t.sol#L194`](../test/invariants/OptimismSuperchainERC20.t.sol#L194) + + + +## Calls to relayERC20 should always succeeds when a message is received from another chain. Actor's balance should only increase by the amount relayed. +**Test:** [`OptimismSuperchainERC20.t.sol#L212`](../test/invariants/OptimismSuperchainERC20.t.sol#L212) diff --git a/packages/contracts-bedrock/test/invariants/OptimismSuperchainERC20.t.sol b/packages/contracts-bedrock/test/invariants/OptimismSuperchainERC20.t.sol index cd86ca33c44c..028a0124e6ca 100644 --- a/packages/contracts-bedrock/test/invariants/OptimismSuperchainERC20.t.sol +++ b/packages/contracts-bedrock/test/invariants/OptimismSuperchainERC20.t.sol @@ -13,12 +13,17 @@ import { IL2ToL2CrossDomainMessenger } from "src/L2/IL2ToL2CrossDomainMessenger. /// @title OptimismSuperchainERC20_User /// @notice Actor contract that interacts with the OptimismSuperchainERC20 contract. contract OptimismSuperchainERC20_User is StdUtils { + address public immutable receiver; + /// @notice Cross domain message data. struct MessageData { bytes32 id; uint256 amount; } + uint256 public totalAmountSent; + uint256 public totalAmountRelayed; + /// @notice Flag to indicate if the test has failed. bool public failed = false; @@ -37,13 +42,14 @@ contract OptimismSuperchainERC20_User is StdUtils { /// @param _vm The Vm contract. /// @param _superchainERC20 The OptimismSuperchainERC20 contract. /// @param _balance The initial balance of the contract. - constructor(Vm _vm, OptimismSuperchainERC20 _superchainERC20, uint256 _balance) { + constructor(Vm _vm, OptimismSuperchainERC20 _superchainERC20, uint256 _balance, address _receiver) { vm = _vm; superchainERC20 = _superchainERC20; // Mint balance to this actor. vm.prank(Predeploys.L2_STANDARD_BRIDGE); superchainERC20.mint(address(this), _balance); + receiver = _receiver; } /// @notice Send ERC20 tokens to another chain. @@ -60,8 +66,9 @@ contract OptimismSuperchainERC20_User is StdUtils { _amount = bound(_amount, 0, superchainERC20.balanceOf(address(this))); // Send the amount. - try superchainERC20.sendERC20(address(this), _amount, _chainId) { + try superchainERC20.sendERC20(receiver, _amount, _chainId) { // Success. + totalAmountSent += _amount; } catch { failed = true; } @@ -99,8 +106,9 @@ contract OptimismSuperchainERC20_User is StdUtils { // Prank the relayERC20 function. // Balance will just go back to our own account. vm.prank(Predeploys.L2_TO_L2_CROSS_DOMAIN_MESSENGER); - try superchainERC20.relayERC20(address(this), address(this), message.amount) { + try superchainERC20.relayERC20(address(this), receiver, message.amount) { // Success. + totalAmountRelayed += message.amount; } catch { failed = true; } @@ -110,12 +118,12 @@ contract OptimismSuperchainERC20_User is StdUtils { } } -/// @title OptimismSuperchainERC20_SendSucceeds_Invariant +/// @title OptimismSuperchainERC20_Invariant /// @notice Invariant test that checks that sending OptimismSuperchainERC20 always succeeds if the actor has a /// sufficient balance to do so and that the actor's balance does not increase out of nowhere. -contract OptimismSuperchainERC20_SendSucceeds_Invariant is Test { +contract OptimismSuperchainERC20_Invariant is Test { /// @notice Starting balance of the contract. - uint256 internal constant STARTING_BALANCE = type(uint256).max; + uint256 public constant STARTING_BALANCE = type(uint128).max; /// @notice The OptimismSuperchainERC20 contract implementation. address internal optimismSuperchainERC20Impl; @@ -126,6 +134,9 @@ contract OptimismSuperchainERC20_SendSucceeds_Invariant is Test { /// @notice The OptimismSuperchainERC20 contract. OptimismSuperchainERC20 internal optimismSuperchainERC20; + /// @notice The address that will receive the tokens when relaying messages + address internal receiver = makeAddr("receiver"); + /// @notice Test setup. function setUp() public { // Deploy the L2ToL2CrossDomainMessenger contract. @@ -141,7 +152,7 @@ contract OptimismSuperchainERC20_SendSucceeds_Invariant is Test { optimismSuperchainERC20 = OptimismSuperchainERC20(_proxy); // Create a new OptimismSuperchainERC20_User actor. - actor = new OptimismSuperchainERC20_User(vm, optimismSuperchainERC20, STARTING_BALANCE); + actor = new OptimismSuperchainERC20_User(vm, optimismSuperchainERC20, STARTING_BALANCE, receiver); // Set the target contract. targetContract(address(actor)); @@ -152,6 +163,11 @@ contract OptimismSuperchainERC20_SendSucceeds_Invariant is Test { selectors[1] = actor.relayMessage.selector; FuzzSelector memory selector = FuzzSelector({ addr: address(actor), selectors: selectors }); targetSelector(selector); + + // Setup assertions + assert(optimismSuperchainERC20.balanceOf(address(actor)) == STARTING_BALANCE); + assert(optimismSuperchainERC20.balanceOf(address(receiver)) == 0); + assert(optimismSuperchainERC20.totalSupply() == STARTING_BALANCE); } /// @notice Sets the bytecode in the implementation address. @@ -172,13 +188,38 @@ contract OptimismSuperchainERC20_SendSucceeds_Invariant is Test { /// @notice Invariant that checks that sending OptimismSuperchainERC20 always succeeds. /// @custom:invariant Calls to sendERC20 should always succeed as long as the actor has enough balance. - /// Actor's balance should also not increase out of nowhere. + /// Actor's balance should also not increase out of nowhere but instead should decrease by the + /// amount sent. function invariant_sendERC20_succeeds() public view { // Assert that the actor has not failed to send OptimismSuperchainERC20. - assertEq(actor.failed(), false); + assertTrue(!actor.failed()); + + // Assert that the actor has sent more than or equal to the amount relayed. + assertTrue(actor.totalAmountSent() >= actor.totalAmountRelayed()); + + // Assert that the actor's balance has decreased by the amount sent. + assertEq(optimismSuperchainERC20.balanceOf(address(actor)), STARTING_BALANCE - actor.totalAmountSent()); + + // Assert that the total supply of the OptimismSuperchainERC20 contract has decreased by the amount unrelayed. + uint256 _unrelayedAmount = actor.totalAmountSent() - actor.totalAmountRelayed(); + assertEq(optimismSuperchainERC20.totalSupply(), STARTING_BALANCE - _unrelayedAmount); + } + + /// @notice Invariant that checks that relaying OptimismSuperchainERC20 always succeeds. + /// @custom:invariant Calls to relayERC20 should always succeeds when a message is received from another chain. + /// Actor's balance should only increase by the amount relayed. + function invariant_relayERC20_succeeds() public view { + // Assert that the actor has not failed to relay OptimismSuperchainERC20. + assertTrue(!actor.failed()); + + // Assert that the actor has sent more than or equal to the amount relayed. + assertTrue(actor.totalAmountSent() >= actor.totalAmountRelayed()); + + // Assert that the actor's balance has increased by the amount relayed. + assertEq(optimismSuperchainERC20.balanceOf(address(receiver)), actor.totalAmountRelayed()); - // Assert that the actor's balance has not somehow increased. - assertLe(optimismSuperchainERC20.balanceOf(address(actor)), STARTING_BALANCE); - assertLe(optimismSuperchainERC20.totalSupply(), STARTING_BALANCE); + // Assert that the total supply of the OptimismSuperchainERC20 contract has decreased by the amount unrelayed. + uint256 _unrelayedAmount = actor.totalAmountSent() - actor.totalAmountRelayed(); + assertEq(optimismSuperchainERC20.totalSupply(), STARTING_BALANCE - _unrelayedAmount); } } From 97377fec360e533f3abc6ffdc12580bb59ec5373 Mon Sep 17 00:00:00 2001 From: agusduha Date: Thu, 8 Aug 2024 14:15:34 -0300 Subject: [PATCH 18/18] fix: gas snapshot --- packages/contracts-bedrock/.gas-snapshot | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/packages/contracts-bedrock/.gas-snapshot b/packages/contracts-bedrock/.gas-snapshot index a97f05678b58..b3ea3b88545e 100644 --- a/packages/contracts-bedrock/.gas-snapshot +++ b/packages/contracts-bedrock/.gas-snapshot @@ -1,7 +1,7 @@ -GasBenchMark_L1CrossDomainMessenger:test_sendMessage_benchmark_0() (gas: 369380) -GasBenchMark_L1CrossDomainMessenger:test_sendMessage_benchmark_1() (gas: 2967520) -GasBenchMark_L1StandardBridge_Deposit:test_depositERC20_benchmark_0() (gas: 561992) -GasBenchMark_L1StandardBridge_Deposit:test_depositERC20_benchmark_1() (gas: 4074035) +GasBenchMark_L1CrossDomainMessenger:test_sendMessage_benchmark_0() (gas: 369356) +GasBenchMark_L1CrossDomainMessenger:test_sendMessage_benchmark_1() (gas: 2967496) +GasBenchMark_L1StandardBridge_Deposit:test_depositERC20_benchmark_0() (gas: 564483) +GasBenchMark_L1StandardBridge_Deposit:test_depositERC20_benchmark_1() (gas: 4076526) GasBenchMark_L1StandardBridge_Deposit:test_depositETH_benchmark_0() (gas: 466947) GasBenchMark_L1StandardBridge_Deposit:test_depositETH_benchmark_1() (gas: 3512629) GasBenchMark_L1StandardBridge_Finalize:test_finalizeETHWithdrawal_benchmark() (gas: 72624)