Commit 35f75534 authored by smartcontracts's avatar smartcontracts Committed by GitHub

feat: introduce SuperchainWETH and ETHLiquidity (#11042)

Introduces the SuperchainWETH and ETHLiquidity contracts. More
information about these contracts can be found in the OP Stack
Specs repository.
parent 6f8aec82
# `ETHLiquidity` Invariants
## Calls to mint/burn repeatedly should never cause the actor's balance to increase beyond the starting balance.
**Test:** [`ETHLiquidity.t.sol#L83`](../test/invariants/ETHLiquidity.t.sol#L83)
...@@ -10,6 +10,7 @@ This directory contains documentation for all defined invariant tests within `co ...@@ -10,6 +10,7 @@ This directory contains documentation for all defined invariant tests within `co
- [Burn.Eth](./Burn.Eth.md) - [Burn.Eth](./Burn.Eth.md)
- [Burn.Gas](./Burn.Gas.md) - [Burn.Gas](./Burn.Gas.md)
- [CrossDomainMessenger](./CrossDomainMessenger.md) - [CrossDomainMessenger](./CrossDomainMessenger.md)
- [ETHLiquidity](./ETHLiquidity.md)
- [Encoding](./Encoding.md) - [Encoding](./Encoding.md)
- [FaultDisputeGame](./FaultDisputeGame.md) - [FaultDisputeGame](./FaultDisputeGame.md)
- [Hashing](./Hashing.md) - [Hashing](./Hashing.md)
...@@ -19,6 +20,7 @@ This directory contains documentation for all defined invariant tests within `co ...@@ -19,6 +20,7 @@ This directory contains documentation for all defined invariant tests within `co
- [OptimismPortal2](./OptimismPortal2.md) - [OptimismPortal2](./OptimismPortal2.md)
- [ResourceMetering](./ResourceMetering.md) - [ResourceMetering](./ResourceMetering.md)
- [SafeCall](./SafeCall.md) - [SafeCall](./SafeCall.md)
- [SuperchainWETH](./SuperchainWETH.md)
- [SystemConfig](./SystemConfig.md) - [SystemConfig](./SystemConfig.md)
<!-- END autoTOC --> <!-- END autoTOC -->
......
# `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)
...@@ -78,8 +78,8 @@ library ForgeArtifacts { ...@@ -78,8 +78,8 @@ library ForgeArtifacts {
string[] memory cmd = new string[](3); string[] memory cmd = new string[](3);
cmd[0] = Executables.bash; cmd[0] = Executables.bash;
cmd[1] = "-c"; cmd[1] = "-c";
cmd[2] = string.concat(Executables.jq, " '.methodIdentifiers | keys' < ", _getForgeArtifactPath(_name)); cmd[2] = string.concat(Executables.jq, " '.methodIdentifiers // {} | keys ' < ", _getForgeArtifactPath(_name));
bytes memory res = Process.run(cmd); bytes memory res = Process.run(cmd, true);
ids_ = stdJson.readStringArray(string(res), ""); ids_ = stdJson.readStringArray(string(res), "");
} }
......
...@@ -245,6 +245,8 @@ contract L2Genesis is Deployer { ...@@ -245,6 +245,8 @@ contract L2Genesis is Deployer {
if (cfg.useInterop()) { if (cfg.useInterop()) {
setCrossL2Inbox(); // 22 setCrossL2Inbox(); // 22
setL2ToL2CrossDomainMessenger(); // 23 setL2ToL2CrossDomainMessenger(); // 23
setSuperchainWETH(); // 24
setETHLiquidity(); // 25
} }
} }
...@@ -475,6 +477,19 @@ contract L2Genesis is Deployer { ...@@ -475,6 +477,19 @@ contract L2Genesis is Deployer {
_setImplementationCode(Predeploys.L2_TO_L2_CROSS_DOMAIN_MESSENGER); _setImplementationCode(Predeploys.L2_TO_L2_CROSS_DOMAIN_MESSENGER);
} }
/// @notice This predeploy is following the safety invariant #1.
/// This contract has no initializer.
function setETHLiquidity() internal {
_setImplementationCode(Predeploys.ETH_LIQUIDITY);
vm.deal(Predeploys.ETH_LIQUIDITY, type(uint248).max);
}
/// @notice This predeploy is following the safety invariant #1.
/// This contract has no initializer.
function setSuperchainWETH() internal {
_setImplementationCode(Predeploys.SUPERCHAIN_WETH);
}
/// @notice Sets all the preinstalls. /// @notice Sets all the preinstalls.
/// Warning: the creator-accounts of the preinstall contracts have 0 nonce values. /// Warning: the creator-accounts of the preinstall contracts have 0 nonce values.
/// When performing a regular user-initiated contract-creation of a preinstall, /// When performing a regular user-initiated contract-creation of a preinstall,
......
...@@ -71,6 +71,10 @@ ...@@ -71,6 +71,10 @@
"initCodeHash": "0xde09fc5ef326a19c5e7542ad9756cc8bc68d857ea3009125c429228dd12ae664", "initCodeHash": "0xde09fc5ef326a19c5e7542ad9756cc8bc68d857ea3009125c429228dd12ae664",
"sourceCodeHash": "0xabd5b4557e9152f26ac70ed8d0e2eb85503861b86b718af5c273752147e6c015" "sourceCodeHash": "0xabd5b4557e9152f26ac70ed8d0e2eb85503861b86b718af5c273752147e6c015"
}, },
"src/L2/ETHLiquidity.sol": {
"initCodeHash": "0x98177562fca0de0dfea5313c9acefe2fdbd73dee5ce6c1232055601f208f0177",
"sourceCodeHash": "0x6dc23ceeed5a63fdc98ba8e5099df1822f3eeaa8c82afb1fa3235ff68a37b274"
},
"src/L2/GasPriceOracle.sol": { "src/L2/GasPriceOracle.sol": {
"initCodeHash": "0xb16f1e370e58c7693fd113a21a1b1e7ccebc03d4f1e5a76786fc27847ef51ead", "initCodeHash": "0xb16f1e370e58c7693fd113a21a1b1e7ccebc03d4f1e5a76786fc27847ef51ead",
"sourceCodeHash": "0x5529ee28aae94904a1c08a8b188f51a39a0f51fbd3b43f1abd4fee7bba57998c" "sourceCodeHash": "0x5529ee28aae94904a1c08a8b188f51a39a0f51fbd3b43f1abd4fee7bba57998c"
...@@ -111,6 +115,10 @@ ...@@ -111,6 +115,10 @@
"initCodeHash": "0xb94145f571e92ee615c6fe903b6568e8aac5fe760b6b65148ffc45d2fb0f5433", "initCodeHash": "0xb94145f571e92ee615c6fe903b6568e8aac5fe760b6b65148ffc45d2fb0f5433",
"sourceCodeHash": "0x8f2a54104e5e7105ba03ba37e3ef9b6684a447245f0e0b787ba4cca12957b97c" "sourceCodeHash": "0x8f2a54104e5e7105ba03ba37e3ef9b6684a447245f0e0b787ba4cca12957b97c"
}, },
"src/L2/SuperchainWETH.sol": {
"initCodeHash": "0x52e302ac749e6a519829e0fb01075638e481e7f010a6438088486a7a4be4601b",
"sourceCodeHash": "0x7c93752288f4414777e01c2962aee929a28aef2c1fccdfeba456f22df0f9aa39"
},
"src/L2/WETH.sol": { "src/L2/WETH.sol": {
"initCodeHash": "0xde72ae96910e95249623c2d695749847e4c4adeaf96a7a35033afd77318a528a", "initCodeHash": "0xde72ae96910e95249623c2d695749847e4c4adeaf96a7a35033afd77318a528a",
"sourceCodeHash": "0xbe200a6cb297a3ca1a7d174a9c886e3f17eb8edf617ad014a2ac4f6c2e2ac7f1" "sourceCodeHash": "0xbe200a6cb297a3ca1a7d174a9c886e3f17eb8edf617ad014a2ac4f6c2e2ac7f1"
......
[
{
"inputs": [],
"name": "burn",
"outputs": [],
"stateMutability": "payable",
"type": "function"
},
{
"inputs": [
{
"internalType": "uint256",
"name": "_amount",
"type": "uint256"
}
],
"name": "mint",
"outputs": [],
"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": "caller",
"type": "address"
},
{
"indexed": false,
"internalType": "uint256",
"name": "value",
"type": "uint256"
}
],
"name": "LiquidityBurned",
"type": "event"
},
{
"anonymous": false,
"inputs": [
{
"indexed": true,
"internalType": "address",
"name": "caller",
"type": "address"
},
{
"indexed": false,
"internalType": "uint256",
"name": "value",
"type": "uint256"
}
],
"name": "LiquidityMinted",
"type": "event"
},
{
"inputs": [],
"name": "NotCustomGasToken",
"type": "error"
},
{
"inputs": [],
"name": "Unauthorized",
"type": "error"
}
]
\ No newline at end of file
[
{
"stateMutability": "payable",
"type": "fallback"
},
{
"stateMutability": "payable",
"type": "receive"
},
{
"inputs": [
{
"internalType": "address",
"name": "",
"type": "address"
},
{
"internalType": "address",
"name": "",
"type": "address"
}
],
"name": "allowance",
"outputs": [
{
"internalType": "uint256",
"name": "",
"type": "uint256"
}
],
"stateMutability": "view",
"type": "function"
},
{
"inputs": [
{
"internalType": "address",
"name": "guy",
"type": "address"
},
{
"internalType": "uint256",
"name": "wad",
"type": "uint256"
}
],
"name": "approve",
"outputs": [
{
"internalType": "bool",
"name": "",
"type": "bool"
}
],
"stateMutability": "nonpayable",
"type": "function"
},
{
"inputs": [
{
"internalType": "address",
"name": "",
"type": "address"
}
],
"name": "balanceOf",
"outputs": [
{
"internalType": "uint256",
"name": "",
"type": "uint256"
}
],
"stateMutability": "view",
"type": "function"
},
{
"inputs": [],
"name": "decimals",
"outputs": [
{
"internalType": "uint8",
"name": "",
"type": "uint8"
}
],
"stateMutability": "view",
"type": "function"
},
{
"inputs": [],
"name": "deposit",
"outputs": [],
"stateMutability": "payable",
"type": "function"
},
{
"inputs": [],
"name": "name",
"outputs": [
{
"internalType": "string",
"name": "",
"type": "string"
}
],
"stateMutability": "view",
"type": "function"
},
{
"inputs": [
{
"internalType": "address",
"name": "dst",
"type": "address"
},
{
"internalType": "uint256",
"name": "wad",
"type": "uint256"
}
],
"name": "relayERC20",
"outputs": [],
"stateMutability": "nonpayable",
"type": "function"
},
{
"inputs": [
{
"internalType": "address",
"name": "dst",
"type": "address"
},
{
"internalType": "uint256",
"name": "wad",
"type": "uint256"
},
{
"internalType": "uint256",
"name": "chainId",
"type": "uint256"
}
],
"name": "sendERC20",
"outputs": [],
"stateMutability": "nonpayable",
"type": "function"
},
{
"inputs": [],
"name": "symbol",
"outputs": [
{
"internalType": "string",
"name": "",
"type": "string"
}
],
"stateMutability": "view",
"type": "function"
},
{
"inputs": [],
"name": "totalSupply",
"outputs": [
{
"internalType": "uint256",
"name": "",
"type": "uint256"
}
],
"stateMutability": "view",
"type": "function"
},
{
"inputs": [
{
"internalType": "address",
"name": "dst",
"type": "address"
},
{
"internalType": "uint256",
"name": "wad",
"type": "uint256"
}
],
"name": "transfer",
"outputs": [
{
"internalType": "bool",
"name": "",
"type": "bool"
}
],
"stateMutability": "nonpayable",
"type": "function"
},
{
"inputs": [
{
"internalType": "address",
"name": "src",
"type": "address"
},
{
"internalType": "address",
"name": "dst",
"type": "address"
},
{
"internalType": "uint256",
"name": "wad",
"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"
},
{
"inputs": [
{
"internalType": "uint256",
"name": "wad",
"type": "uint256"
}
],
"name": "withdraw",
"outputs": [],
"stateMutability": "nonpayable",
"type": "function"
},
{
"anonymous": false,
"inputs": [
{
"indexed": true,
"internalType": "address",
"name": "src",
"type": "address"
},
{
"indexed": true,
"internalType": "address",
"name": "guy",
"type": "address"
},
{
"indexed": false,
"internalType": "uint256",
"name": "wad",
"type": "uint256"
}
],
"name": "Approval",
"type": "event"
},
{
"anonymous": false,
"inputs": [
{
"indexed": true,
"internalType": "address",
"name": "dst",
"type": "address"
},
{
"indexed": false,
"internalType": "uint256",
"name": "wad",
"type": "uint256"
}
],
"name": "Deposit",
"type": "event"
},
{
"anonymous": false,
"inputs": [
{
"indexed": true,
"internalType": "address",
"name": "_to",
"type": "address"
},
{
"indexed": false,
"internalType": "uint256",
"name": "_amount",
"type": "uint256"
}
],
"name": "RelayERC20",
"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": "SendERC20",
"type": "event"
},
{
"anonymous": false,
"inputs": [
{
"indexed": true,
"internalType": "address",
"name": "src",
"type": "address"
},
{
"indexed": true,
"internalType": "address",
"name": "dst",
"type": "address"
},
{
"indexed": false,
"internalType": "uint256",
"name": "wad",
"type": "uint256"
}
],
"name": "Transfer",
"type": "event"
},
{
"anonymous": false,
"inputs": [
{
"indexed": true,
"internalType": "address",
"name": "src",
"type": "address"
},
{
"indexed": false,
"internalType": "uint256",
"name": "wad",
"type": "uint256"
}
],
"name": "Withdrawal",
"type": "event"
},
{
"inputs": [],
"name": "NotCustomGasToken",
"type": "error"
},
{
"inputs": [],
"name": "Unauthorized",
"type": "error"
}
]
\ No newline at end of file
[
{
"bytes": "32",
"label": "balanceOf",
"offset": 0,
"slot": "0",
"type": "mapping(address => uint256)"
},
{
"bytes": "32",
"label": "allowance",
"offset": 0,
"slot": "1",
"type": "mapping(address => mapping(address => uint256))"
}
]
\ No newline at end of file
// SPDX-License-Identifier: MIT
pragma solidity 0.8.15;
import { Unauthorized, NotCustomGasToken } from "src/libraries/errors/CommonErrors.sol";
import { Predeploys } from "src/libraries/Predeploys.sol";
import { L1Block } from "src/L2/L1Block.sol";
import { SafeSend } from "src/universal/SafeSend.sol";
import { ISemver } from "src/universal/ISemver.sol";
/// @title ETHLiquidity
/// @notice The ETHLiquidity contract allows other contracts to access ETH liquidity without
/// needing to modify the EVM to generate new ETH.
contract ETHLiquidity is ISemver {
/// @notice Emitted when an address burns ETH liquidity.
event LiquidityBurned(address indexed caller, uint256 value);
/// @notice Emitted when an address mints ETH liquidity.
event LiquidityMinted(address indexed caller, uint256 value);
/// @notice Semantic version.
/// @custom:semver 1.0.0-beta.1
string public constant version = "1.0.0-beta.1";
/// @notice Allows an address to lock ETH liquidity into this contract.
function burn() external payable {
if (msg.sender != Predeploys.SUPERCHAIN_WETH) revert Unauthorized();
if (L1Block(Predeploys.L1_BLOCK_ATTRIBUTES).isCustomGasToken()) revert NotCustomGasToken();
emit LiquidityBurned(msg.sender, msg.value);
}
/// @notice Allows an address to unlock ETH liquidity from this contract.
/// @param _amount The amount of liquidity to unlock.
function mint(uint256 _amount) external {
if (msg.sender != Predeploys.SUPERCHAIN_WETH) revert Unauthorized();
if (L1Block(Predeploys.L1_BLOCK_ATTRIBUTES).isCustomGasToken()) revert NotCustomGasToken();
new SafeSend{ value: _amount }(payable(msg.sender));
emit LiquidityMinted(msg.sender, _amount);
}
}
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.0;
import { IERC20 } from "@openzeppelin/contracts/token/ERC20/IERC20.sol";
/// @title ISuperchainERC20Extensions
/// @notice Interface for the extensions to the ERC20 standard that are used by SuperchainERC20.
/// Exists in case developers are already importing the ERC20 interface separately and
/// 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);
/// @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 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.
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;
}
/// @title ISuperchainERC20
/// @notice Combines the ERC20 interface with the SuperchainERC20Extensions interface.
interface ISuperchainERC20 is IERC20, ISuperchainERC20Extensions { }
// SPDX-License-Identifier: MIT
pragma solidity 0.8.15;
import { WETH98 } from "src/dispute/weth/WETH98.sol";
import { Unauthorized, NotCustomGasToken } from "src/libraries/errors/CommonErrors.sol";
import { Predeploys } from "src/libraries/Predeploys.sol";
import { L1Block } from "src/L2/L1Block.sol";
import { IL2ToL2CrossDomainMessenger } from "src/L2/IL2ToL2CrossDomainMessenger.sol";
import { ETHLiquidity } from "src/L2/ETHLiquidity.sol";
import { ISuperchainERC20Extensions } from "src/L2/ISuperchainERC20.sol";
import { ISemver } from "src/universal/ISemver.sol";
/// @title SuperchainWETH
/// @notice SuperchainWETH is a version of WETH that can be freely transfrered between chains
/// within the superchain. SuperchainWETH can be converted into native ETH on chains that
/// do not use a custom gas token.
contract SuperchainWETH is WETH98, ISuperchainERC20Extensions, ISemver {
/// @notice Semantic version.
/// @custom:semver 1.0.0-beta.1
string public constant version = "1.0.0-beta.1";
/// @inheritdoc WETH98
function deposit() public payable override {
if (L1Block(Predeploys.L1_BLOCK_ATTRIBUTES).isCustomGasToken()) revert NotCustomGasToken();
super.deposit();
}
/// @inheritdoc WETH98
function withdraw(uint256 wad) public override {
if (L1Block(Predeploys.L1_BLOCK_ATTRIBUTES).isCustomGasToken()) revert NotCustomGasToken();
super.withdraw(wad);
}
/// @inheritdoc ISuperchainERC20Extensions
function sendERC20(address dst, uint256 wad, uint256 chainId) public {
// Burn from user's balance.
_burn(msg.sender, wad);
// Burn to ETHLiquidity contract.
if (!L1Block(Predeploys.L1_BLOCK_ATTRIBUTES).isCustomGasToken()) {
ETHLiquidity(Predeploys.ETH_LIQUIDITY).burn{ value: wad }();
}
// Send message to other chain.
IL2ToL2CrossDomainMessenger(Predeploys.L2_TO_L2_CROSS_DOMAIN_MESSENGER).sendMessage({
_destination: chainId,
_target: address(this),
_message: abi.encodeCall(this.relayERC20, (dst, wad))
});
// Emit event.
emit SendERC20(msg.sender, dst, wad, chainId);
}
/// @inheritdoc ISuperchainERC20Extensions
function relayERC20(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();
if (messenger.crossDomainMessageSender() != address(this)) revert Unauthorized();
// Mint from ETHLiquidity contract.
if (!L1Block(Predeploys.L1_BLOCK_ATTRIBUTES).isCustomGasToken()) {
ETHLiquidity(Predeploys.ETH_LIQUIDITY).mint(wad);
}
// Mint to user's balance.
_mint(dst, wad);
// Emit event.
emit RelayERC20(dst, wad);
}
/// @notice Mints WETH to an address.
/// @param guy The address to mint WETH to.
/// @param wad The amount of WETH to mint.
function _mint(address guy, uint256 wad) internal {
balanceOf[guy] += wad;
emit Transfer(address(0), guy, wad);
}
/// @notice Burns WETH from an address.
/// @param guy The address to burn WETH from.
/// @param wad The amount of WETH to burn.
function _burn(address guy, uint256 wad) internal {
require(balanceOf[guy] >= wad);
balanceOf[guy] -= wad;
emit Transfer(guy, address(0), wad);
}
}
...@@ -50,7 +50,7 @@ contract WETH98 is IWETH { ...@@ -50,7 +50,7 @@ contract WETH98 is IWETH {
} }
/// @inheritdoc IWETH /// @inheritdoc IWETH
function deposit() public payable { function deposit() public payable virtual {
balanceOf[msg.sender] += msg.value; balanceOf[msg.sender] += msg.value;
emit Deposit(msg.sender, msg.value); emit Deposit(msg.sender, msg.value);
} }
......
...@@ -89,6 +89,12 @@ library Predeploys { ...@@ -89,6 +89,12 @@ library Predeploys {
/// @notice Address of the L2ToL2CrossDomainMessenger predeploy. /// @notice Address of the L2ToL2CrossDomainMessenger predeploy.
address internal constant L2_TO_L2_CROSS_DOMAIN_MESSENGER = 0x4200000000000000000000000000000000000023; address internal constant L2_TO_L2_CROSS_DOMAIN_MESSENGER = 0x4200000000000000000000000000000000000023;
/// @notice Address of the SuperchainWETH predeploy.
address internal constant SUPERCHAIN_WETH = 0x4200000000000000000000000000000000000024;
/// @notice Address of the ETHLiquidity predeploy.
address internal constant ETH_LIQUIDITY = 0x4200000000000000000000000000000000000025;
/// @notice Returns the name of the predeploy at the given address. /// @notice Returns the name of the predeploy at the given address.
function getName(address _addr) internal pure returns (string memory out_) { function getName(address _addr) internal pure returns (string memory out_) {
require(isPredeployNamespace(_addr), "Predeploys: address must be a predeploy"); require(isPredeployNamespace(_addr), "Predeploys: address must be a predeploy");
...@@ -115,6 +121,8 @@ library Predeploys { ...@@ -115,6 +121,8 @@ library Predeploys {
if (_addr == LEGACY_ERC20_ETH) return "LegacyERC20ETH"; if (_addr == LEGACY_ERC20_ETH) return "LegacyERC20ETH";
if (_addr == CROSS_L2_INBOX) return "CrossL2Inbox"; if (_addr == CROSS_L2_INBOX) return "CrossL2Inbox";
if (_addr == L2_TO_L2_CROSS_DOMAIN_MESSENGER) return "L2ToL2CrossDomainMessenger"; if (_addr == L2_TO_L2_CROSS_DOMAIN_MESSENGER) return "L2ToL2CrossDomainMessenger";
if (_addr == SUPERCHAIN_WETH) return "SuperchainWETH";
if (_addr == ETH_LIQUIDITY) return "ETHLiquidity";
revert("Predeploys: unnamed predeploy"); revert("Predeploys: unnamed predeploy");
} }
...@@ -131,7 +139,8 @@ library Predeploys { ...@@ -131,7 +139,8 @@ library Predeploys {
|| _addr == L2_ERC721_BRIDGE || _addr == L1_BLOCK_ATTRIBUTES || _addr == L2_TO_L1_MESSAGE_PASSER || _addr == L2_ERC721_BRIDGE || _addr == L1_BLOCK_ATTRIBUTES || _addr == L2_TO_L1_MESSAGE_PASSER
|| _addr == OPTIMISM_MINTABLE_ERC721_FACTORY || _addr == PROXY_ADMIN || _addr == BASE_FEE_VAULT || _addr == OPTIMISM_MINTABLE_ERC721_FACTORY || _addr == PROXY_ADMIN || _addr == BASE_FEE_VAULT
|| _addr == L1_FEE_VAULT || _addr == SCHEMA_REGISTRY || _addr == EAS || _addr == GOVERNANCE_TOKEN || _addr == L1_FEE_VAULT || _addr == SCHEMA_REGISTRY || _addr == EAS || _addr == GOVERNANCE_TOKEN
|| (_useInterop && _addr == CROSS_L2_INBOX) || (_useInterop && _addr == L2_TO_L2_CROSS_DOMAIN_MESSENGER); || (_useInterop && _addr == CROSS_L2_INBOX) || (_useInterop && _addr == L2_TO_L2_CROSS_DOMAIN_MESSENGER)
|| (_useInterop && _addr == SUPERCHAIN_WETH) || (_useInterop && _addr == ETH_LIQUIDITY);
} }
function isPredeployNamespace(address _addr) internal pure returns (bool) { function isPredeployNamespace(address _addr) internal pure returns (bool) {
......
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.0;
/// @notice Error for an unauthorized CALLER.
error Unauthorized();
/// @notice Error for when a method is called that only works when using a custom gas token.
error OnlyCustomGasToken();
/// @notice Error for when a method is called that only works when NOT using a custom gas token.
error NotCustomGasToken();
/// @notice Error for when a transfer via call fails.
error TransferFailed();
// SPDX-License-Identifier: MIT // SPDX-License-Identifier: MIT
pragma solidity 0.8.15; pragma solidity 0.8.15;
import { IFaucetAuthModule } from "./authmodules/IFaucetAuthModule.sol"; import { IFaucetAuthModule } from "src/periphery/faucet/authmodules/IFaucetAuthModule.sol";
import { SafeCall } from "../../libraries/SafeCall.sol"; import { SafeCall } from "src/libraries/SafeCall.sol";
import { SafeSend } from "src/universal/SafeSend.sol";
/// @title SafeSend
/// @notice Sends ETH to a recipient account without triggering any code.
contract SafeSend {
/// @param _recipient Account to send ETH to.
constructor(address payable _recipient) payable {
selfdestruct(_recipient);
}
}
/// @title Faucet /// @title Faucet
/// @notice Faucet contract that drips ETH to users. /// @notice Faucet contract that drips ETH to users.
......
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.0;
/// @title SafeSend
/// @notice Sends ETH to a recipient account without triggering any code.
contract SafeSend {
/// @param _recipient Account to send ETH to.
constructor(address payable _recipient) payable {
selfdestruct(_recipient);
}
}
// SPDX-License-Identifier: MIT
pragma solidity 0.8.15;
// Testing utilities
import { CommonTest } from "test/setup/CommonTest.sol";
// Error imports
import { Unauthorized, NotCustomGasToken } from "src/libraries/errors/CommonErrors.sol";
/// @title ETHLiquidity_Test
/// @notice Contract for testing the ETHLiquidity contract.
contract ETHLiquidity_Test is CommonTest {
/// @notice Emitted when an address burns ETH liquidity.
event LiquidityBurned(address indexed caller, uint256 value);
/// @notice Emitted when an address mints ETH liquidity.
event LiquidityMinted(address indexed caller, uint256 value);
/// @notice The starting balance of the ETHLiquidity contract.
uint256 public constant STARTING_LIQUIDITY_BALANCE = type(uint248).max;
/// @notice Test setup.
function setUp() public virtual override {
super.enableInterop();
super.setUp();
}
/// @notice Tests that contract is set up with the correct starting balance.
function test_setup_succeeds() public view {
// Assert
assertEq(address(ethLiquidity).balance, STARTING_LIQUIDITY_BALANCE);
}
/// @notice Tests that the burn function can always be called by an authorized caller.
/// @param _amount Amount of ETH (in wei) to call the burn function with.
function testFuzz_burn_fromAuthorizedCaller_succeeds(uint256 _amount) public {
// Assume
_amount = bound(_amount, 0, type(uint248).max - 1);
// Arrange
vm.deal(address(superchainWeth), _amount);
// Act
vm.expectEmit(address(ethLiquidity));
emit LiquidityBurned(address(superchainWeth), _amount);
vm.prank(address(superchainWeth));
ethLiquidity.burn{ value: _amount }();
// Assert
assertEq(address(superchainWeth).balance, 0);
assertEq(address(ethLiquidity).balance, STARTING_LIQUIDITY_BALANCE + _amount);
}
/// @notice Tests that the burn function always reverts when called by an unauthorized caller.
/// @param _amount Amount of ETH (in wei) to call the burn function with.
/// @param _caller Address of the caller to call the burn function with.
function testFuzz_burn_fromUnauthorizedCaller_fails(uint256 _amount, address _caller) public {
// Assume
vm.assume(_caller != address(superchainWeth));
_amount = bound(_amount, 0, type(uint248).max - 1);
// Arrange
vm.deal(_caller, _amount);
// Act
vm.prank(_caller);
vm.expectRevert(Unauthorized.selector);
ethLiquidity.burn{ value: _amount }();
// Assert
assertEq(_caller.balance, _amount);
assertEq(address(ethLiquidity).balance, STARTING_LIQUIDITY_BALANCE);
}
/// @notice Tests that the burn function reverts when called on a custom gas token chain.
/// @param _amount Amount of ETH (in wei) to call the burn function with.
function testFuzz_burn_fromCustomGasTokenChain_fails(uint256 _amount) public {
// Assume
_amount = bound(_amount, 0, type(uint248).max - 1);
// Arrange
vm.deal(address(superchainWeth), _amount);
vm.mockCall(address(l1Block), abi.encodeCall(l1Block.isCustomGasToken, ()), abi.encode(true));
// Act
vm.prank(address(superchainWeth));
vm.expectRevert(NotCustomGasToken.selector);
ethLiquidity.burn{ value: _amount }();
// Assert
assertEq(address(superchainWeth).balance, _amount);
assertEq(address(ethLiquidity).balance, STARTING_LIQUIDITY_BALANCE);
}
/// @notice Tests that the mint function fails when the amount requested is greater than the
/// available balance. In practice this should never happen because the starting
/// balance is expected to be uint248 wei, the total ETH supply is far less than that
/// amount, and the only contract that pulls from here is the SuperchainWETH contract
/// which will always burn ETH somewhere before minting it somewhere else. It needs to
/// be a system-wide invariant that this condition is never triggered in the first
/// place but it is the behavior we expect if it does happen.
function test_mint_moreThanAvailableBalance_fails() public {
// Arrange
uint256 amount = STARTING_LIQUIDITY_BALANCE + 1;
// Act
vm.expectRevert();
ethLiquidity.mint(amount);
// Assert
assertEq(address(superchainWeth).balance, 0);
assertEq(address(ethLiquidity).balance, STARTING_LIQUIDITY_BALANCE);
assertEq(superchainWeth.balanceOf(address(ethLiquidity)), 0);
}
/// @notice Tests that the mint function can always be called by an authorized caller.
/// @param _amount Amount of ETH (in wei) to call the mint function with.
function testFuzz_mint_fromAuthorizedCaller_succeeds(uint256 _amount) public {
// Assume
_amount = bound(_amount, 0, type(uint248).max - 1);
// Arrange
// Nothing to arrange.
// Act
vm.expectEmit(address(ethLiquidity));
emit LiquidityMinted(address(superchainWeth), _amount);
vm.prank(address(superchainWeth));
ethLiquidity.mint(_amount);
// Assert
assertEq(address(superchainWeth).balance, _amount);
assertEq(address(ethLiquidity).balance, STARTING_LIQUIDITY_BALANCE - _amount);
assertEq(superchainWeth.balanceOf(address(ethLiquidity)), 0);
}
/// @notice Tests that the mint function always reverts when called by an unauthorized caller.
/// @param _amount Amount of ETH (in wei) to call the mint function with.
/// @param _caller Address of the caller to call the mint function with.
function testFuzz_mint_fromUnauthorizedCaller_fails(uint256 _amount, address _caller) public {
// Assume
vm.assume(_caller != address(superchainWeth));
vm.assume(address(_caller).balance == 0);
_amount = bound(_amount, 0, type(uint248).max - 1);
// Arrange
// Nothing to arrange.
// Act
vm.prank(_caller);
vm.expectRevert(Unauthorized.selector);
ethLiquidity.mint(_amount);
// Assert
assertEq(_caller.balance, 0);
assertEq(address(ethLiquidity).balance, STARTING_LIQUIDITY_BALANCE);
assertEq(superchainWeth.balanceOf(address(ethLiquidity)), 0);
}
/// @notice Tests that the mint function reverts when called on a custom gas token chain.
/// @param _amount Amount of ETH (in wei) to call the mint function with.
function testFuzz_mint_fromCustomGasTokenChain_fails(uint256 _amount) public {
// Assume
_amount = bound(_amount, 0, type(uint248).max - 1);
// Arrange
vm.mockCall(address(l1Block), abi.encodeCall(l1Block.isCustomGasToken, ()), abi.encode(true));
// Act
vm.prank(address(superchainWeth));
vm.expectRevert(NotCustomGasToken.selector);
ethLiquidity.mint(_amount);
// Assert
assertEq(address(superchainWeth).balance, 0);
assertEq(address(ethLiquidity).balance, STARTING_LIQUIDITY_BALANCE);
assertEq(superchainWeth.balanceOf(address(ethLiquidity)), 0);
}
}
// SPDX-License-Identifier: MIT
pragma solidity 0.8.15;
// Testing utilities
import { CommonTest } from "test/setup/CommonTest.sol";
// Contract imports
import { Unauthorized, NotCustomGasToken } from "src/libraries/errors/CommonErrors.sol";
import { Predeploys } from "src/libraries/Predeploys.sol";
import { IL2ToL2CrossDomainMessenger } from "src/L2/IL2ToL2CrossDomainMessenger.sol";
import { ETHLiquidity } from "src/L2/ETHLiquidity.sol";
/// @title SuperchainWETH_Test
/// @notice Contract for testing the SuperchainWETH contract.
contract SuperchainWETH_Test is CommonTest {
/// @notice Emitted when a transfer is made.
event Transfer(address indexed src, address indexed dst, uint256 wad);
/// @notice Emitted when a deposit is made.
event Deposit(address indexed dst, uint256 wad);
/// @notice Emitted when a withdrawal is made.
event Withdrawal(address indexed src, uint256 wad);
/// @notice Emitted when an ERC20 is sent.
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);
/// @notice Test setup.
function setUp() public virtual override {
super.enableInterop();
super.setUp();
}
/// @notice Tests that the deposit function can be called on a non-custom gas token chain.
/// @param _amount The amount of WETH to send.
function testFuzz_deposit_fromNonCustomGasTokenChain_succeeds(uint256 _amount) public {
// Assume
_amount = bound(_amount, 0, type(uint248).max - 1);
// Arrange
vm.deal(alice, _amount);
// Act
vm.expectEmit(address(superchainWeth));
emit Deposit(alice, _amount);
vm.prank(alice);
superchainWeth.deposit{ value: _amount }();
// Assert
assertEq(alice.balance, 0);
assertEq(superchainWeth.balanceOf(alice), _amount);
}
/// @notice Tests that the deposit function reverts when called on a custom gas token chain.
/// @param _amount The amount of WETH to send.
function testFuzz_deposit_fromCustomGasTokenChain_fails(uint256 _amount) public {
// Assume
_amount = bound(_amount, 0, type(uint248).max - 1);
// Arrange
vm.deal(address(alice), _amount);
vm.mockCall(address(l1Block), abi.encodeCall(l1Block.isCustomGasToken, ()), abi.encode(true));
// Act
vm.prank(alice);
vm.expectRevert(NotCustomGasToken.selector);
superchainWeth.deposit{ value: _amount }();
// Assert
assertEq(alice.balance, _amount);
assertEq(superchainWeth.balanceOf(alice), 0);
}
/// @notice Tests that the withdraw function can be called on a non-custom gas token chain.
/// @param _amount The amount of WETH to send.
function testFuzz_withdraw_fromNonCustomGasTokenChain_succeeds(uint256 _amount) public {
// Assume
_amount = bound(_amount, 0, type(uint248).max - 1);
// Arrange
vm.deal(alice, _amount);
vm.prank(alice);
superchainWeth.deposit{ value: _amount }();
// Act
vm.expectEmit(address(superchainWeth));
emit Withdrawal(alice, _amount);
vm.prank(alice);
superchainWeth.withdraw(_amount);
// Assert
assertEq(alice.balance, _amount);
assertEq(superchainWeth.balanceOf(alice), 0);
}
/// @notice Tests that the withdraw function reverts when called on a custom gas token chain.
/// @param _amount The amount of WETH to send.
function testFuzz_withdraw_fromCustomGasTokenChain_fails(uint256 _amount) public {
// Assume
_amount = bound(_amount, 0, type(uint248).max - 1);
// Arrange
vm.deal(alice, _amount);
vm.prank(alice);
superchainWeth.deposit{ value: _amount }();
vm.mockCall(address(l1Block), abi.encodeCall(l1Block.isCustomGasToken, ()), abi.encode(true));
// Act
vm.prank(alice);
vm.expectRevert(NotCustomGasToken.selector);
superchainWeth.withdraw(_amount);
// Assert
assertEq(alice.balance, 0);
assertEq(superchainWeth.balanceOf(alice), _amount);
}
/// @notice Tests that the sendERC20 function always succeeds when called with a sufficient
/// balance no matter the sender, amount, recipient, or chain ID.
/// @param _amount The amount of WETH to send.
/// @param _caller The address of the caller.
/// @param _recipient The address of the recipient.
/// @param _chainId The chain ID to send the WETH to.
function testFuzz_sendERC20_sufficientBalance_succeeds(
uint256 _amount,
address _caller,
address _recipient,
uint256 _chainId
)
public
{
// Assume
_amount = bound(_amount, 0, type(uint248).max - 1);
// Arrange
vm.deal(_caller, _amount);
vm.prank(_caller);
superchainWeth.deposit{ value: _amount }();
// Act
vm.expectEmit(address(superchainWeth));
emit Transfer(_caller, address(0), _amount);
vm.expectEmit(address(superchainWeth));
emit SendERC20(_caller, _recipient, _amount, _chainId);
vm.expectCall(Predeploys.ETH_LIQUIDITY, abi.encodeCall(ETHLiquidity.burn, ()), 1);
vm.expectCall(
Predeploys.L2_TO_L2_CROSS_DOMAIN_MESSENGER,
abi.encodeCall(
IL2ToL2CrossDomainMessenger.sendMessage,
(_chainId, address(superchainWeth), abi.encodeCall(superchainWeth.relayERC20, (_recipient, _amount)))
),
1
);
vm.prank(_caller);
superchainWeth.sendERC20(_recipient, _amount, _chainId);
// Assert
assertEq(_caller.balance, 0);
assertEq(superchainWeth.balanceOf(_caller), 0);
}
/// @notice Tests that the sendERC20 function can be called with a sufficient balance on a
/// custom gas token chain. Also tests that the proper calls are made and the proper
/// events are emitted but ETH is not burned via the ETHLiquidity contract.
/// @param _amount The amount of WETH to send.
/// @param _chainId The chain ID to send the WETH to.
function testFuzz_sendERC20_sufficientFromCustomGasTokenChain_succeeds(uint256 _amount, uint256 _chainId) public {
// Assume
_amount = bound(_amount, 0, type(uint248).max - 1);
// Arrange
vm.deal(alice, _amount);
vm.prank(alice);
superchainWeth.deposit{ value: _amount }();
vm.mockCall(address(l1Block), abi.encodeCall(l1Block.isCustomGasToken, ()), abi.encode(true));
// Act
vm.expectEmit(address(superchainWeth));
emit Transfer(alice, address(0), _amount);
vm.expectEmit(address(superchainWeth));
emit SendERC20(alice, bob, _amount, _chainId);
vm.expectCall(Predeploys.ETH_LIQUIDITY, abi.encodeCall(ETHLiquidity.burn, ()), 0);
vm.expectCall(
Predeploys.L2_TO_L2_CROSS_DOMAIN_MESSENGER,
abi.encodeCall(
IL2ToL2CrossDomainMessenger.sendMessage,
(_chainId, address(superchainWeth), abi.encodeCall(superchainWeth.relayERC20, (bob, _amount)))
),
1
);
vm.prank(alice);
superchainWeth.sendERC20(bob, _amount, _chainId);
// Assert
assertEq(alice.balance, 0);
assertEq(superchainWeth.balanceOf(alice), 0);
}
/// @notice Tests that the sendERC20 function reverts when called with insufficient balance.
/// @param _amount The amount of WETH to send.
/// @param _chainId The chain ID to send the WETH to.
function testFuzz_sendERC20_insufficientBalance_fails(uint256 _amount, uint256 _chainId) public {
// Assume
_amount = bound(_amount, 0, type(uint248).max - 1);
// Arrange
vm.deal(alice, _amount);
vm.prank(alice);
superchainWeth.deposit{ value: _amount }();
// Act
vm.expectRevert();
superchainWeth.sendERC20(bob, _amount + 1, _chainId);
// Assert
assertEq(alice.balance, 0);
assertEq(superchainWeth.balanceOf(alice), _amount);
}
/// @notice Tests that the relayERC20 function can be called from the
/// 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 {
// Assume
_amount = bound(_amount, 0, type(uint248).max - 1);
// Arrange
vm.mockCall(
Predeploys.L2_TO_L2_CROSS_DOMAIN_MESSENGER,
abi.encodeCall(IL2ToL2CrossDomainMessenger.crossDomainMessageSender, ()),
abi.encode(address(superchainWeth))
);
// Act
vm.expectEmit(address(superchainWeth));
emit RelayERC20(bob, _amount);
vm.expectCall(Predeploys.ETH_LIQUIDITY, abi.encodeCall(ETHLiquidity.mint, (_amount)), 1);
vm.prank(Predeploys.L2_TO_L2_CROSS_DOMAIN_MESSENGER);
superchainWeth.relayERC20(bob, _amount);
// Assert
assertEq(address(superchainWeth).balance, _amount);
assertEq(superchainWeth.balanceOf(bob), _amount);
}
/// @notice Tests that the relayERC20 function can be called from the
/// L2ToL2CrossDomainMessenger as long as the crossDomainMessageSender is the
/// 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 {
// Assume
_amount = bound(_amount, 0, type(uint248).max - 1);
// Arrange
vm.mockCall(
Predeploys.L2_TO_L2_CROSS_DOMAIN_MESSENGER,
abi.encodeCall(IL2ToL2CrossDomainMessenger.crossDomainMessageSender, ()),
abi.encode(address(superchainWeth))
);
vm.mockCall(address(l1Block), abi.encodeCall(l1Block.isCustomGasToken, ()), abi.encode(true));
// Act
vm.expectEmit(address(superchainWeth));
emit RelayERC20(bob, _amount);
vm.expectCall(Predeploys.ETH_LIQUIDITY, abi.encodeCall(ETHLiquidity.mint, (_amount)), 0);
vm.prank(Predeploys.L2_TO_L2_CROSS_DOMAIN_MESSENGER);
superchainWeth.relayERC20(bob, _amount);
// Assert
assertEq(address(superchainWeth).balance, 0);
assertEq(superchainWeth.balanceOf(bob), _amount);
}
/// @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 {
// Assume
_amount = bound(_amount, 0, type(uint248).max - 1);
// Arrange
// Nothing to arrange.
// Act
vm.expectRevert(Unauthorized.selector);
vm.prank(alice);
superchainWeth.relayERC20(bob, _amount);
// Assert
assertEq(address(superchainWeth).balance, 0);
assertEq(superchainWeth.balanceOf(bob), 0);
}
/// @notice Tests that the relayERC20 function reverts when called from the
/// 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 {
// Assume
_amount = bound(_amount, 0, type(uint248).max - 1);
// Arrange
vm.mockCall(
Predeploys.L2_TO_L2_CROSS_DOMAIN_MESSENGER,
abi.encodeCall(IL2ToL2CrossDomainMessenger.crossDomainMessageSender, ()),
abi.encode(address(alice))
);
// Act
vm.expectRevert(Unauthorized.selector);
vm.prank(Predeploys.L2_TO_L2_CROSS_DOMAIN_MESSENGER);
superchainWeth.relayERC20(bob, _amount);
// Assert
assertEq(address(superchainWeth).balance, 0);
assertEq(superchainWeth.balanceOf(bob), 0);
}
}
...@@ -150,8 +150,8 @@ contract L2GenesisTest is Test { ...@@ -150,8 +150,8 @@ contract L2GenesisTest is Test {
// 2 predeploys do not have proxies // 2 predeploys do not have proxies
assertEq(getCodeCount(_path, "Proxy.sol:Proxy"), Predeploys.PREDEPLOY_COUNT - 2); assertEq(getCodeCount(_path, "Proxy.sol:Proxy"), Predeploys.PREDEPLOY_COUNT - 2);
// 19 proxies have the implementation set if useInterop is true and 17 if useInterop is false // 21 proxies have the implementation set if useInterop is true and 17 if useInterop is false
assertEq(getPredeployCountWithSlotSet(_path, Constants.PROXY_IMPLEMENTATION_ADDRESS), _useInterop ? 19 : 17); assertEq(getPredeployCountWithSlotSet(_path, Constants.PROXY_IMPLEMENTATION_ADDRESS), _useInterop ? 21 : 17);
// All proxies except 2 have the proxy 1967 admin slot set to the proxy admin // All proxies except 2 have the proxy 1967 admin slot set to the proxy admin
assertEq( assertEq(
......
...@@ -242,7 +242,7 @@ contract DeputyGuardianModule_NoPortalCollisions_Test is DeputyGuardianModule_Te ...@@ -242,7 +242,7 @@ contract DeputyGuardianModule_NoPortalCollisions_Test is DeputyGuardianModule_Te
string[] memory excludes = new string[](2); string[] memory excludes = new string[](2);
excludes[0] = "src/L1/OptimismPortal2.sol"; excludes[0] = "src/L1/OptimismPortal2.sol";
excludes[1] = "src/dispute/lib/*"; excludes[1] = "src/dispute/lib/*";
Abi[] memory abis = ForgeArtifacts.getContractFunctionAbis("src/{L1,dispute,universal}/", excludes); Abi[] memory abis = ForgeArtifacts.getContractFunctionAbis("src/{L1,dispute,universal}", excludes);
for (uint256 i; i < abis.length; i++) { for (uint256 i; i < abis.length; i++) {
for (uint256 j; j < abis[i].entries.length; j++) { for (uint256 j; j < abis[i].entries.length; j++) {
bytes4 sel = abis[i].entries[j].sel; bytes4 sel = abis[i].entries[j].sel;
......
// SPDX-License-Identifier: MIT
pragma solidity 0.8.15;
import { StdUtils } from "forge-std/Test.sol";
import { Vm } from "forge-std/Vm.sol";
import { Predeploys } from "src/libraries/Predeploys.sol";
import { ETHLiquidity } from "src/L2/ETHLiquidity.sol";
import { CommonTest } from "test/setup/CommonTest.sol";
/// @title ETHLiquidity_User
/// @notice Actor contract that interacts with the ETHLiquidity contract. Always pretends to be the
/// SuperchainWETH contract since it's the only contract that can use ETHLiquidity.
contract ETHLiquidity_User is StdUtils {
/// @notice Flag to indicate if the test has failed.
bool public failed = false;
/// @notice The Vm contract.
Vm internal vm;
/// @notice The ETHLiquidity contract.
ETHLiquidity internal liquidity;
/// @param _vm The Vm contract.
/// @param _liquidity The ETHLiquidity contract.
/// @param _balance The initial balance of the contract.
constructor(Vm _vm, ETHLiquidity _liquidity, uint256 _balance) {
vm = _vm;
liquidity = _liquidity;
vm.deal(Predeploys.SUPERCHAIN_WETH, _balance);
}
/// @notice Mint ETH liquidity.
/// @param _amount The amount of ETH to mint.
function mint(uint256 _amount) public {
vm.prank(Predeploys.SUPERCHAIN_WETH);
liquidity.mint(_amount);
}
/// @notice Burn ETH liquidity.
/// @param _amount The amount of ETH to burn.
function burn(uint256 _amount) public {
vm.prank(Predeploys.SUPERCHAIN_WETH);
liquidity.burn{ value: _amount }();
}
}
/// @title ETHLiquidity_MintBurn_Invariant
/// @notice Invariant that checks that minting/burning ETH liquidity does not cause the actor's
/// balance to magically increase beyond the starting balance.
contract ETHLiquidity_MintBurn_Invariant is CommonTest {
/// @notice Starting balance of the contract.
uint256 internal constant STARTING_BALANCE = type(uint248).max;
/// @notice The ETHLiquidity_User actor.
ETHLiquidity_User internal actor;
/// @notice Test setup.
function setUp() public override {
super.enableInterop();
super.setUp();
// Create a new ETHLiquidity_User actor.
actor = new ETHLiquidity_User(vm, ethLiquidity, STARTING_BALANCE);
// Set the target contract.
targetContract(address(actor));
// Set the target selectors.
bytes4[] memory selectors = new bytes4[](2);
selectors[0] = actor.mint.selector;
selectors[1] = actor.burn.selector;
FuzzSelector memory selector = FuzzSelector({ addr: address(actor), selectors: selectors });
targetSelector(selector);
}
/// @notice Invariant that checks that repeatedly minting/burning does not cause the actor's
/// balance to increase beyond the starting balance.
/// @custom:invariant Calls to mint/burn repeatedly should never cause the actor's balance to
/// increase beyond the starting balance.
function invariant_mintburn_maintainsBalance() public view {
// Assert that the actor's balance has not somehow increased.
assertLe(address(actor).balance, type(uint248).max);
}
}
// SPDX-License-Identifier: MIT
pragma solidity 0.8.15;
import { StdUtils } from "forge-std/Test.sol";
import { Vm } from "forge-std/Vm.sol";
import { Predeploys } from "src/libraries/Predeploys.sol";
import { SuperchainWETH } from "src/L2/SuperchainWETH.sol";
import { IL2ToL2CrossDomainMessenger } from "src/L2/IL2ToL2CrossDomainMessenger.sol";
import { CommonTest } from "test/setup/CommonTest.sol";
/// @title SuperchainWETH_User
/// @notice Actor contract that interacts with the SuperchainWETH contract.
contract SuperchainWETH_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 SuperchainWETH contract.
SuperchainWETH internal weth;
/// @notice Mapping of sent messages.
mapping(bytes32 => bool) internal sent;
/// @notice Array of unrelayed messages.
MessageData[] internal unrelayed;
/// @param _vm The Vm contract.
/// @param _weth The SuperchainWETH contract.
/// @param _balance The initial balance of the contract.
constructor(Vm _vm, SuperchainWETH _weth, uint256 _balance) {
vm = _vm;
weth = _weth;
vm.deal(address(this), _balance);
}
/// @notice Allow the contract to receive ETH.
receive() external payable { }
/// @notice Deposit ETH into the contract.
/// @param _amount The amount of ETH to deposit.
function deposit(uint256 _amount) public {
// Bound deposit amount to our ETH balance.
_amount = bound(_amount, 0, address(this).balance);
// Deposit the amount.
try weth.deposit{ value: _amount }() {
// Success.
} catch {
failed = true;
}
}
/// @notice Withdraw ETH from the contract.
/// @param _amount The amount of ETH to withdraw.
function withdraw(uint256 _amount) public {
// Bound withdraw amount to our WETH balance.
_amount = bound(_amount, 0, weth.balanceOf(address(this)));
// Withdraw the amount.
try weth.withdraw(_amount) {
// Success.
} catch {
failed = true;
}
}
/// @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 WETH balance.
_amount = bound(_amount, 0, weth.balanceOf(address(this)));
// Send the amount.
try weth.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(weth))
);
// 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) {
// Success.
} catch {
failed = true;
}
// Remove the message from the unrelayed list.
unrelayed.pop();
}
}
/// @title SuperchainWETH_SendSucceeds_Invariant
/// @notice Invariant test that checks that sending WETH 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 SuperchainWETH_SendSucceeds_Invariant is CommonTest {
/// @notice Starting balance of the contract.
uint256 internal constant STARTING_BALANCE = type(uint248).max;
/// @notice The SuperchainWETH_User actor.
SuperchainWETH_User internal actor;
/// @notice Test setup.
function setUp() public override {
super.enableInterop();
super.setUp();
// Create a new SuperchainWETH_User actor.
actor = new SuperchainWETH_User(vm, superchainWeth, STARTING_BALANCE);
// Set the target contract.
targetContract(address(actor));
// Set the target selectors.
bytes4[] memory selectors = new bytes4[](4);
selectors[0] = actor.deposit.selector;
selectors[1] = actor.withdraw.selector;
selectors[2] = actor.sendERC20.selector;
selectors[3] = actor.relayMessage.selector;
FuzzSelector memory selector = FuzzSelector({ addr: address(actor), selectors: selectors });
targetSelector(selector);
}
/// @notice Invariant that checks that sending WETH always succeeds.
/// @custom:invariant 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.
function invariant_sendERC20_succeeds() public view {
// Assert that the actor has not failed to send WETH.
assertEq(actor.failed(), false);
// Assert that the actor's balance has not somehow increased.
assertLe(address(actor).balance, STARTING_BALANCE);
}
}
...@@ -41,6 +41,8 @@ import { Vm } from "forge-std/Vm.sol"; ...@@ -41,6 +41,8 @@ import { Vm } from "forge-std/Vm.sol";
import { SuperchainConfig } from "src/L1/SuperchainConfig.sol"; import { SuperchainConfig } from "src/L1/SuperchainConfig.sol";
import { DataAvailabilityChallenge } from "src/L1/DataAvailabilityChallenge.sol"; import { DataAvailabilityChallenge } from "src/L1/DataAvailabilityChallenge.sol";
import { WETH } from "src/L2/WETH.sol"; import { WETH } from "src/L2/WETH.sol";
import { SuperchainWETH } from "src/L2/SuperchainWETH.sol";
import { ETHLiquidity } from "src/L2/ETHLiquidity.sol";
/// @title Setup /// @title Setup
/// @dev This contact is responsible for setting up the contracts in state. It currently /// @dev This contact is responsible for setting up the contracts in state. It currently
...@@ -94,6 +96,8 @@ contract Setup { ...@@ -94,6 +96,8 @@ contract Setup {
LegacyMessagePasser legacyMessagePasser = LegacyMessagePasser(Predeploys.LEGACY_MESSAGE_PASSER); LegacyMessagePasser legacyMessagePasser = LegacyMessagePasser(Predeploys.LEGACY_MESSAGE_PASSER);
GovernanceToken governanceToken = GovernanceToken(Predeploys.GOVERNANCE_TOKEN); GovernanceToken governanceToken = GovernanceToken(Predeploys.GOVERNANCE_TOKEN);
WETH weth = WETH(payable(Predeploys.WETH)); WETH weth = WETH(payable(Predeploys.WETH));
SuperchainWETH superchainWeth = SuperchainWETH(payable(Predeploys.SUPERCHAIN_WETH));
ETHLiquidity ethLiquidity = ETHLiquidity(Predeploys.ETH_LIQUIDITY);
/// @dev Deploys the Deploy contract without including its bytecode in the bytecode /// @dev Deploys the Deploy contract without including its bytecode in the bytecode
/// of this contract by fetching the bytecode dynamically using `vm.getCode()`. /// of this contract by fetching the bytecode dynamically using `vm.getCode()`.
...@@ -211,6 +215,8 @@ contract Setup { ...@@ -211,6 +215,8 @@ contract Setup {
labelPredeploy(Predeploys.EAS); labelPredeploy(Predeploys.EAS);
labelPredeploy(Predeploys.SCHEMA_REGISTRY); labelPredeploy(Predeploys.SCHEMA_REGISTRY);
labelPredeploy(Predeploys.WETH); labelPredeploy(Predeploys.WETH);
labelPredeploy(Predeploys.SUPERCHAIN_WETH);
labelPredeploy(Predeploys.ETH_LIQUIDITY);
// L2 Preinstalls // L2 Preinstalls
labelPreinstall(Preinstalls.MultiCall3); labelPreinstall(Preinstalls.MultiCall3);
......
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