Commit 27a1bfa9 authored by AgusDuha's avatar AgusDuha Committed by GitHub

feat: introduce OptimismSuperchainERC20 (#11256)

* feat: introduce OptimismSuperchainERC20

* fix: contract fixes

* feat: add snapshots and semver

* test: add supports interface tests

* test: add invariant test

* feat: add parameters to the RelayERC20 event

* fix: typo

* fix: from param description

* fix: event signature and interface pragma

* feat: add initializer

* feat: use unstructured storage and OZ v5

* feat: update superchain erc20 interfaces

* fix: adapt storage to ERC7201

* test: add initializable OZ v5 test

* fix: invariant docs

* fix: ERC165 implementation

* test: improve superc20 invariant (#11)

* fix: gas snapshot

---------
Co-authored-by: default avatar0xng <ng@defi.sucks>
Co-authored-by: default avatarDisco <131301107+0xDiscotech@users.noreply.github.com>
parent 2024e2f5
...@@ -26,3 +26,6 @@ ...@@ -26,3 +26,6 @@
[submodule "packages/contracts-bedrock/lib/automate"] [submodule "packages/contracts-bedrock/lib/automate"]
path = packages/contracts-bedrock/lib/automate path = packages/contracts-bedrock/lib/automate
url = https://github.com/gelatodigital/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
...@@ -13,6 +13,7 @@ optimizer_runs = 999999 ...@@ -13,6 +13,7 @@ optimizer_runs = 999999
remappings = [ remappings = [
'@openzeppelin/contracts-upgradeable/=lib/openzeppelin-contracts-upgradeable/contracts', '@openzeppelin/contracts-upgradeable/=lib/openzeppelin-contracts-upgradeable/contracts',
'@openzeppelin/contracts/=lib/openzeppelin-contracts/contracts', '@openzeppelin/contracts/=lib/openzeppelin-contracts/contracts',
'@openzeppelin/contracts-v5/=lib/openzeppelin-contracts-v5/contracts',
'@rari-capital/solmate/=lib/solmate', '@rari-capital/solmate/=lib/solmate',
'@lib-keccak/=lib/lib-keccak/contracts/lib', '@lib-keccak/=lib/lib-keccak/contracts/lib',
'@solady/=lib/solady/src', '@solady/=lib/solady/src',
......
# `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 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)
...@@ -18,6 +18,7 @@ This directory contains documentation for all defined invariant tests within `co ...@@ -18,6 +18,7 @@ This directory contains documentation for all defined invariant tests within `co
- [L2OutputOracle](./L2OutputOracle.md) - [L2OutputOracle](./L2OutputOracle.md)
- [OptimismPortal](./OptimismPortal.md) - [OptimismPortal](./OptimismPortal.md)
- [OptimismPortal2](./OptimismPortal2.md) - [OptimismPortal2](./OptimismPortal2.md)
- [OptimismSuperchainERC20](./OptimismSuperchainERC20.md)
- [ResourceMetering](./ResourceMetering.md) - [ResourceMetering](./ResourceMetering.md)
- [SafeCall](./SafeCall.md) - [SafeCall](./SafeCall.md)
- [SuperchainWETH](./SuperchainWETH.md) - [SuperchainWETH](./SuperchainWETH.md)
......
# `SuperchainWETH` Invariants # `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. ## 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#L174`](../test/invariants/SuperchainWETH.t.sol#L174) **Test:** [`SuperchainWETH.t.sol#L181`](../test/invariants/SuperchainWETH.t.sol#L181)
Subproject commit dbb6104ce834628e473d2173bbc9d47f81a9eec3
...@@ -111,13 +111,17 @@ ...@@ -111,13 +111,17 @@
"initCodeHash": "0xe390be1390edc38fd879d7620538560076d7fcf3ef9debce327a1877d96d3ff0", "initCodeHash": "0xe390be1390edc38fd879d7620538560076d7fcf3ef9debce327a1877d96d3ff0",
"sourceCodeHash": "0x20f77dc5a02869c6885b73347fa9e7d2bbc4eaf8a2313f7e7435e456001f7a75" "sourceCodeHash": "0x20f77dc5a02869c6885b73347fa9e7d2bbc4eaf8a2313f7e7435e456001f7a75"
}, },
"src/L2/OptimismSuperchainERC20.sol": {
"initCodeHash": "0xd49214518ea1a30a43fac09f28b2cee9be570894a500cef342762c9820a070b0",
"sourceCodeHash": "0x6943d40010dcbd1d51dc3668d0a154fbb1568ea49ebcf3aa039d65ef6eab321b"
},
"src/L2/SequencerFeeVault.sol": { "src/L2/SequencerFeeVault.sol": {
"initCodeHash": "0xb94145f571e92ee615c6fe903b6568e8aac5fe760b6b65148ffc45d2fb0f5433", "initCodeHash": "0xb94145f571e92ee615c6fe903b6568e8aac5fe760b6b65148ffc45d2fb0f5433",
"sourceCodeHash": "0x8f2a54104e5e7105ba03ba37e3ef9b6684a447245f0e0b787ba4cca12957b97c" "sourceCodeHash": "0x8f2a54104e5e7105ba03ba37e3ef9b6684a447245f0e0b787ba4cca12957b97c"
}, },
"src/L2/SuperchainWETH.sol": { "src/L2/SuperchainWETH.sol": {
"initCodeHash": "0x52e302ac749e6a519829e0fb01075638e481e7f010a6438088486a7a4be4601b", "initCodeHash": "0x599e948350c70d699f8a8be945abffd126097de97fade056d29767128320fe75",
"sourceCodeHash": "0x7c93752288f4414777e01c2962aee929a28aef2c1fccdfeba456f22df0f9aa39" "sourceCodeHash": "0x3df29ee1321418914d88ce303b521bf8267ef234b919870b26639d08d7f806bd"
}, },
"src/L2/WETH.sol": { "src/L2/WETH.sol": {
"initCodeHash": "0xde72ae96910e95249623c2d695749847e4c4adeaf96a7a35033afd77318a528a", "initCodeHash": "0xde72ae96910e95249623c2d695749847e4c4adeaf96a7a35033afd77318a528a",
......
...@@ -109,6 +109,11 @@ ...@@ -109,6 +109,11 @@
}, },
{ {
"inputs": [ "inputs": [
{
"internalType": "address",
"name": "from",
"type": "address"
},
{ {
"internalType": "address", "internalType": "address",
"name": "dst", "name": "dst",
...@@ -303,13 +308,25 @@ ...@@ -303,13 +308,25 @@
{ {
"indexed": true, "indexed": true,
"internalType": "address", "internalType": "address",
"name": "_to", "name": "from",
"type": "address"
},
{
"indexed": true,
"internalType": "address",
"name": "to",
"type": "address" "type": "address"
}, },
{ {
"indexed": false, "indexed": false,
"internalType": "uint256", "internalType": "uint256",
"name": "_amount", "name": "amount",
"type": "uint256"
},
{
"indexed": false,
"internalType": "uint256",
"name": "source",
"type": "uint256" "type": "uint256"
} }
], ],
...@@ -322,25 +339,25 @@ ...@@ -322,25 +339,25 @@
{ {
"indexed": true, "indexed": true,
"internalType": "address", "internalType": "address",
"name": "_from", "name": "from",
"type": "address" "type": "address"
}, },
{ {
"indexed": true, "indexed": true,
"internalType": "address", "internalType": "address",
"name": "_to", "name": "to",
"type": "address" "type": "address"
}, },
{ {
"indexed": false, "indexed": false,
"internalType": "uint256", "internalType": "uint256",
"name": "_amount", "name": "amount",
"type": "uint256" "type": "uint256"
}, },
{ {
"indexed": false, "indexed": false,
"internalType": "uint256", "internalType": "uint256",
"name": "_chainId", "name": "destination",
"type": "uint256" "type": "uint256"
} }
], ],
......
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.0;
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 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.
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 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 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;
/// @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 { }
...@@ -9,27 +9,30 @@ import { IERC20 } from "@openzeppelin/contracts/token/ERC20/IERC20.sol"; ...@@ -9,27 +9,30 @@ import { IERC20 } from "@openzeppelin/contracts/token/ERC20/IERC20.sol";
/// importing the full SuperchainERC20 interface would cause conflicting imports. /// importing the full SuperchainERC20 interface would cause conflicting imports.
interface ISuperchainERC20Extensions { interface ISuperchainERC20Extensions {
/// @notice Emitted when tokens are sent from one chain to another. /// @notice Emitted when tokens are sent from one chain to another.
/// @param _from Address of the sender. /// @param from Address of the sender.
/// @param _to Address of the recipient. /// @param to Address of the recipient.
/// @param _amount Number of tokens sent. /// @param amount Number of tokens sent.
/// @param _chainId Chain ID of the recipient. /// @param destination Chain ID of the destination chain.
event SendERC20(address indexed _from, address indexed _to, uint256 _amount, uint256 _chainId); event SendERC20(address indexed from, address indexed to, uint256 amount, uint256 destination);
/// @notice Emitted when token sends are relayed to this chain. /// @notice Emitted whenever tokens are successfully relayed on this chain.
/// @param _to Address of the recipient. /// @param from Address of the msg.sender of sendERC20 on the source chain.
/// @param _amount Number of tokens sent. /// @param to Address of the recipient.
event RelayERC20(address indexed _to, uint256 _amount); /// @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. /// @notice Sends tokens to some target address on another chain.
/// @param _to Address of the recipient. /// @param _to Address to send tokens to.
/// @param _amount Number of tokens to send. /// @param _amount Amount of tokens to send.
/// @param _chainId Chain ID of the recipient. /// @param _chainId Chain ID of the destination chain.
function sendERC20(address _to, uint256 _amount, uint256 _chainId) external; function sendERC20(address _to, uint256 _amount, uint256 _chainId) external;
/// @notice Relays a send of tokens to this chain. /// @notice Relays tokens received from another chain.
/// @param _to Address of the recipient. /// @param _from Address of the msg.sender of sendERC20 on the source chain.
/// @param _amount Number of tokens sent. /// @param _to Address to relay tokens to.
function relayERC20(address _to, uint256 _amount) external; /// @param _amount Amount of tokens to relay.
function relayERC20(address _from, address _to, uint256 _amount) external;
} }
/// @title ISuperchainERC20 /// @title ISuperchainERC20
......
// SPDX-License-Identifier: MIT
pragma solidity 0.8.25;
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";
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.
error CallerNotL2ToL2CrossDomainMessenger();
/// @notice Thrown when attempting to relay a message and the cross domain message sender is not this
/// OptimismSuperchainERC20.
error InvalidCrossDomainSender();
/// @notice Thrown when attempting to mint or burn tokens and the function caller is not the StandardBridge.
error OnlyBridge();
/// @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 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 IOptimismSuperchainERC20Extension, ERC20, ISemver, Initializable, ERC165 {
/// @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 Storage slot that the OptimismSuperchainERC20Metadata struct is stored at.
/// keccak256(abi.encode(uint256(keccak256("optimismSuperchainERC20.metadata")) - 1)) & ~bytes32(uint256(0xff));
bytes32 internal constant OPTIMISM_SUPERCHAIN_ERC20_METADATA_SLOT =
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;
/// @notice Name of the token
string name;
/// @notice Symbol of the token
string symbol;
/// @notice Decimals of the token
uint8 decimals;
}
/// @notice Returns the storage for the OptimismSuperchainERC20Metadata.
function _getMetadataStorage() private pure returns (OptimismSuperchainERC20Metadata storage _storage) {
assembly {
_storage.slot := OPTIMISM_SUPERCHAIN_ERC20_METADATA_SLOT
}
}
/// @notice A modifier that only allows the bridge to call
modifier onlyBridge() {
if (msg.sender != BRIDGE) revert OnlyBridge();
_;
}
/// @notice Semantic version.
/// @custom:semver 1.0.0-beta.1
string public constant version = "1.0.0-beta.1";
/// @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
{
OptimismSuperchainERC20Metadata storage _storage = _getMetadataStorage();
_storage.remoteToken = _remoteToken;
_storage.name = _name;
_storage.symbol = _symbol;
_storage.decimals = _decimals;
}
/// @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 {
if (_to == address(0)) revert ZeroAddress();
_mint(_to, _amount);
emit Mint(_to, _amount);
}
/// @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 {
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, (msg.sender, _to, _amount));
IL2ToL2CrossDomainMessenger(MESSENGER).sendMessage(_chainId, address(this), _message);
emit SendERC20(msg.sender, _to, _amount, _chainId);
}
/// @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 {
if (_to == address(0)) revert ZeroAddress();
if (msg.sender != MESSENGER) revert CallerNotL2ToL2CrossDomainMessenger();
if (IL2ToL2CrossDomainMessenger(MESSENGER).crossDomainMessageSender() != address(this)) {
revert InvalidCrossDomainSender();
}
uint256 source = IL2ToL2CrossDomainMessenger(MESSENGER).crossDomainMessageSource();
_mint(_to, _amount);
emit RelayERC20(_from, _to, _amount, source);
}
/// @notice Returns the address of the corresponding version of this token on the remote chain.
function remoteToken() public view override returns (address) {
return _getMetadataStorage().remoteToken;
}
/// @notice Returns the name of the token.
function name() public view virtual override returns (string memory) {
return _getMetadataStorage().name;
}
/// @notice Returns the symbol of the token.
function symbol() public view virtual override returns (string memory) {
return _getMetadataStorage().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 _getMetadataStorage().decimals;
}
/// @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) public view virtual override returns (bool) {
return
_interfaceId == type(IOptimismSuperchainERC20Extension).interfaceId || super.supportsInterface(_interfaceId);
}
}
...@@ -45,7 +45,7 @@ contract SuperchainWETH is WETH98, ISuperchainERC20Extensions, ISemver { ...@@ -45,7 +45,7 @@ contract SuperchainWETH is WETH98, ISuperchainERC20Extensions, ISemver {
IL2ToL2CrossDomainMessenger(Predeploys.L2_TO_L2_CROSS_DOMAIN_MESSENGER).sendMessage({ IL2ToL2CrossDomainMessenger(Predeploys.L2_TO_L2_CROSS_DOMAIN_MESSENGER).sendMessage({
_destination: chainId, _destination: chainId,
_target: address(this), _target: address(this),
_message: abi.encodeCall(this.relayERC20, (dst, wad)) _message: abi.encodeCall(this.relayERC20, (msg.sender, dst, wad))
}); });
// Emit event. // Emit event.
...@@ -53,7 +53,7 @@ contract SuperchainWETH is WETH98, ISuperchainERC20Extensions, ISemver { ...@@ -53,7 +53,7 @@ contract SuperchainWETH is WETH98, ISuperchainERC20Extensions, ISemver {
} }
/// @inheritdoc ISuperchainERC20Extensions /// @inheritdoc ISuperchainERC20Extensions
function relayERC20(address dst, uint256 wad) external { function relayERC20(address from, address dst, uint256 wad) external {
// Receive message from other chain. // Receive message from other chain.
IL2ToL2CrossDomainMessenger messenger = IL2ToL2CrossDomainMessenger(Predeploys.L2_TO_L2_CROSS_DOMAIN_MESSENGER); IL2ToL2CrossDomainMessenger messenger = IL2ToL2CrossDomainMessenger(Predeploys.L2_TO_L2_CROSS_DOMAIN_MESSENGER);
if (msg.sender != address(messenger)) revert Unauthorized(); if (msg.sender != address(messenger)) revert Unauthorized();
...@@ -64,11 +64,14 @@ contract SuperchainWETH is WETH98, ISuperchainERC20Extensions, ISemver { ...@@ -64,11 +64,14 @@ contract SuperchainWETH is WETH98, ISuperchainERC20Extensions, ISemver {
ETHLiquidity(Predeploys.ETH_LIQUIDITY).mint(wad); ETHLiquidity(Predeploys.ETH_LIQUIDITY).mint(wad);
} }
// Get source chain ID.
uint256 source = messenger.crossDomainMessageSource();
// Mint to user's balance. // Mint to user's balance.
_mint(dst, wad); _mint(dst, wad);
// Emit event. // Emit event.
emit RelayERC20(dst, wad); emit RelayERC20(from, dst, wad, source);
} }
/// @notice Mints WETH to an address. /// @notice Mints WETH to an address.
......
...@@ -26,7 +26,7 @@ contract SuperchainWETH_Test is CommonTest { ...@@ -26,7 +26,7 @@ contract SuperchainWETH_Test is CommonTest {
event SendERC20(address indexed _from, address indexed _to, uint256 _amount, uint256 _chainId); event SendERC20(address indexed _from, address indexed _to, uint256 _amount, uint256 _chainId);
/// @notice Emitted when an ERC20 send is relayed. /// @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. /// @notice Test setup.
function setUp() public virtual override { function setUp() public virtual override {
...@@ -151,7 +151,11 @@ contract SuperchainWETH_Test is CommonTest { ...@@ -151,7 +151,11 @@ contract SuperchainWETH_Test is CommonTest {
Predeploys.L2_TO_L2_CROSS_DOMAIN_MESSENGER, Predeploys.L2_TO_L2_CROSS_DOMAIN_MESSENGER,
abi.encodeCall( abi.encodeCall(
IL2ToL2CrossDomainMessenger.sendMessage, IL2ToL2CrossDomainMessenger.sendMessage,
(_chainId, address(superchainWeth), abi.encodeCall(superchainWeth.relayERC20, (_recipient, _amount))) (
_chainId,
address(superchainWeth),
abi.encodeCall(superchainWeth.relayERC20, (_caller, _recipient, _amount))
)
), ),
1 1
); );
...@@ -189,7 +193,7 @@ contract SuperchainWETH_Test is CommonTest { ...@@ -189,7 +193,7 @@ contract SuperchainWETH_Test is CommonTest {
Predeploys.L2_TO_L2_CROSS_DOMAIN_MESSENGER, Predeploys.L2_TO_L2_CROSS_DOMAIN_MESSENGER,
abi.encodeCall( abi.encodeCall(
IL2ToL2CrossDomainMessenger.sendMessage, IL2ToL2CrossDomainMessenger.sendMessage,
(_chainId, address(superchainWeth), abi.encodeCall(superchainWeth.relayERC20, (bob, _amount))) (_chainId, address(superchainWeth), abi.encodeCall(superchainWeth.relayERC20, (alice, bob, _amount)))
), ),
1 1
); );
...@@ -227,7 +231,7 @@ contract SuperchainWETH_Test is CommonTest { ...@@ -227,7 +231,7 @@ contract SuperchainWETH_Test is CommonTest {
/// L2ToL2CrossDomainMessenger as long as the crossDomainMessageSender is the /// L2ToL2CrossDomainMessenger as long as the crossDomainMessageSender is the
/// SuperchainWETH contract. /// SuperchainWETH contract.
/// @param _amount The amount of WETH to send. /// @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 // Assume
_amount = bound(_amount, 0, type(uint248).max - 1); _amount = bound(_amount, 0, type(uint248).max - 1);
...@@ -237,13 +241,18 @@ contract SuperchainWETH_Test is CommonTest { ...@@ -237,13 +241,18 @@ contract SuperchainWETH_Test is CommonTest {
abi.encodeCall(IL2ToL2CrossDomainMessenger.crossDomainMessageSender, ()), abi.encodeCall(IL2ToL2CrossDomainMessenger.crossDomainMessageSender, ()),
abi.encode(address(superchainWeth)) abi.encode(address(superchainWeth))
); );
vm.mockCall(
Predeploys.L2_TO_L2_CROSS_DOMAIN_MESSENGER,
abi.encodeCall(IL2ToL2CrossDomainMessenger.crossDomainMessageSource, ()),
abi.encode(_chainId)
);
// Act // Act
vm.expectEmit(address(superchainWeth)); 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.expectCall(Predeploys.ETH_LIQUIDITY, abi.encodeCall(ETHLiquidity.mint, (_amount)), 1);
vm.prank(Predeploys.L2_TO_L2_CROSS_DOMAIN_MESSENGER); vm.prank(Predeploys.L2_TO_L2_CROSS_DOMAIN_MESSENGER);
superchainWeth.relayERC20(bob, _amount); superchainWeth.relayERC20(_sender, bob, _amount);
// Assert // Assert
assertEq(address(superchainWeth).balance, _amount); assertEq(address(superchainWeth).balance, _amount);
...@@ -255,7 +264,13 @@ contract SuperchainWETH_Test is CommonTest { ...@@ -255,7 +264,13 @@ contract SuperchainWETH_Test is CommonTest {
/// SuperchainWETH contract, even when the chain is a custom gas token chain. Shows /// 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. /// that ETH is not minted in this case but the SuperchainWETH balance is updated.
/// @param _amount The amount of WETH to send. /// @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 // Assume
_amount = bound(_amount, 0, type(uint248).max - 1); _amount = bound(_amount, 0, type(uint248).max - 1);
...@@ -265,14 +280,19 @@ contract SuperchainWETH_Test is CommonTest { ...@@ -265,14 +280,19 @@ contract SuperchainWETH_Test is CommonTest {
abi.encodeCall(IL2ToL2CrossDomainMessenger.crossDomainMessageSender, ()), abi.encodeCall(IL2ToL2CrossDomainMessenger.crossDomainMessageSender, ()),
abi.encode(address(superchainWeth)) 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)); vm.mockCall(address(l1Block), abi.encodeCall(l1Block.isCustomGasToken, ()), abi.encode(true));
// Act // Act
vm.expectEmit(address(superchainWeth)); 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.expectCall(Predeploys.ETH_LIQUIDITY, abi.encodeCall(ETHLiquidity.mint, (_amount)), 0);
vm.prank(Predeploys.L2_TO_L2_CROSS_DOMAIN_MESSENGER); vm.prank(Predeploys.L2_TO_L2_CROSS_DOMAIN_MESSENGER);
superchainWeth.relayERC20(bob, _amount); superchainWeth.relayERC20(_sender, bob, _amount);
// Assert // Assert
assertEq(address(superchainWeth).balance, 0); assertEq(address(superchainWeth).balance, 0);
...@@ -282,7 +302,7 @@ contract SuperchainWETH_Test is CommonTest { ...@@ -282,7 +302,7 @@ contract SuperchainWETH_Test is CommonTest {
/// @notice Tests that the relayERC20 function reverts when not called from the /// @notice Tests that the relayERC20 function reverts when not called from the
/// L2ToL2CrossDomainMessenger. /// L2ToL2CrossDomainMessenger.
/// @param _amount The amount of WETH to send. /// @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 // Assume
_amount = bound(_amount, 0, type(uint248).max - 1); _amount = bound(_amount, 0, type(uint248).max - 1);
...@@ -292,7 +312,7 @@ contract SuperchainWETH_Test is CommonTest { ...@@ -292,7 +312,7 @@ contract SuperchainWETH_Test is CommonTest {
// Act // Act
vm.expectRevert(Unauthorized.selector); vm.expectRevert(Unauthorized.selector);
vm.prank(alice); vm.prank(alice);
superchainWeth.relayERC20(bob, _amount); superchainWeth.relayERC20(_sender, bob, _amount);
// Assert // Assert
assertEq(address(superchainWeth).balance, 0); assertEq(address(superchainWeth).balance, 0);
...@@ -303,7 +323,7 @@ contract SuperchainWETH_Test is CommonTest { ...@@ -303,7 +323,7 @@ contract SuperchainWETH_Test is CommonTest {
/// L2ToL2CrossDomainMessenger but the crossDomainMessageSender is not the /// L2ToL2CrossDomainMessenger but the crossDomainMessageSender is not the
/// SuperchainWETH contract. /// SuperchainWETH contract.
/// @param _amount The amount of WETH to send. /// @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 // Assume
_amount = bound(_amount, 0, type(uint248).max - 1); _amount = bound(_amount, 0, type(uint248).max - 1);
...@@ -317,7 +337,7 @@ contract SuperchainWETH_Test is CommonTest { ...@@ -317,7 +337,7 @@ contract SuperchainWETH_Test is CommonTest {
// Act // Act
vm.expectRevert(Unauthorized.selector); vm.expectRevert(Unauthorized.selector);
vm.prank(Predeploys.L2_TO_L2_CROSS_DOMAIN_MESSENGER); vm.prank(Predeploys.L2_TO_L2_CROSS_DOMAIN_MESSENGER);
superchainWeth.relayERC20(bob, _amount); superchainWeth.relayERC20(_sender, bob, _amount);
// Assert // Assert
assertEq(address(superchainWeth).balance, 0); assertEq(address(superchainWeth).balance, 0);
......
// 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 {
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;
/// @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, 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.
/// @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(receiver, _amount, _chainId) {
// Success.
totalAmountSent += _amount;
} 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(uint256 _source) 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))
);
// 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), receiver, message.amount) {
// Success.
totalAmountRelayed += message.amount;
} catch {
failed = true;
}
// Remove the message from the unrelayed list.
unrelayed.pop();
}
}
/// @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_Invariant is Test {
/// @notice Starting balance of the contract.
uint256 public constant STARTING_BALANCE = type(uint128).max;
/// @notice The OptimismSuperchainERC20 contract implementation.
address internal optimismSuperchainERC20Impl;
/// @notice The OptimismSuperchainERC20_User actor.
OptimismSuperchainERC20_User internal actor;
/// @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.
address _impl = _setImplementationCode(Predeploys.L2_TO_L2_CROSS_DOMAIN_MESSENGER);
_setProxyCode(Predeploys.L2_TO_L2_CROSS_DOMAIN_MESSENGER, _impl);
// 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, receiver);
// 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);
// 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.
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("universal/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 but instead should decrease by the
/// amount sent.
function invariant_sendERC20_succeeds() public view {
// Assert that the actor has not failed to send 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 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 total supply of the OptimismSuperchainERC20 contract has decreased by the amount unrelayed.
uint256 _unrelayedAmount = actor.totalAmountSent() - actor.totalAmountRelayed();
assertEq(optimismSuperchainERC20.totalSupply(), STARTING_BALANCE - _unrelayedAmount);
}
}
...@@ -103,7 +103,7 @@ contract SuperchainWETH_User is StdUtils { ...@@ -103,7 +103,7 @@ contract SuperchainWETH_User is StdUtils {
} }
/// @notice Relay a message from another chain. /// @notice Relay a message from another chain.
function relayMessage() public { function relayMessage(uint256 _source) public {
// Make sure there are unrelayed messages. // Make sure there are unrelayed messages.
if (unrelayed.length == 0) { if (unrelayed.length == 0) {
return; return;
...@@ -120,10 +120,17 @@ contract SuperchainWETH_User is StdUtils { ...@@ -120,10 +120,17 @@ contract SuperchainWETH_User is StdUtils {
abi.encode(address(weth)) 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. // Prank the relayERC20 function.
// Balance will just go back to our own account. // Balance will just go back to our own account.
vm.prank(Predeploys.L2_TO_L2_CROSS_DOMAIN_MESSENGER); 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. // Success.
} catch { } catch {
failed = true; failed = true;
......
...@@ -352,7 +352,8 @@ contract Initializer_Test is Bridge_Initializer { ...@@ -352,7 +352,8 @@ contract Initializer_Test is Bridge_Initializer {
// Ensure that all L1, L2 `Initializable` contracts are accounted for, in addition to // Ensure that all L1, L2 `Initializable` contracts are accounted for, in addition to
// OptimismMintableERC20FactoryImpl, OptimismMintableERC20FactoryProxy, OptimismPortal2, // OptimismMintableERC20FactoryImpl, OptimismMintableERC20FactoryProxy, OptimismPortal2,
// DisputeGameFactoryImpl, DisputeGameFactoryProxy, DelayedWETHImpl, DelayedWETHProxy. // 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. // Attempt to re-initialize all contracts within the `contracts` array.
for (uint256 i; i < contracts.length; i++) { for (uint256 i; i < contracts.length; i++) {
......
// 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);
}
}
}
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment