Commit 9a062d54 authored by clabby's avatar clabby Committed by GitHub

feat(ctb): Add `OptimismPortal2` (#9267)

* Add `OptimismPortal2`

* old portal

* new portal

* fmt
parent f6affa41
......@@ -31,6 +31,10 @@
"initCodeHash": "0x54658799b54481f56acb6481db4f37ad830a8238a8fd592c96e8b1a2b60e0627",
"sourceCodeHash": "0xdc27421279afb6c3b26fc8c589c5d213695f666c74d2c2c41cb7df719d172f37"
},
"src/L1/OptimismPortal2.sol": {
"initCodeHash": "0x3b140ef8e85169a59c626974aeb31be43bc58c5a48592611d74004925732ab5f",
"sourceCodeHash": "0xf28140c13ea7cefb9525ee106268e7d482f3e6e13d8b300d3b1423de8ac173f3"
},
"src/L1/ProtocolVersions.sol": {
"initCodeHash": "0x72cd467e8bcf019c02675d72ab762e088bcc9cc0f1a4e9f587fa4589f7fdd1b8",
"sourceCodeHash": "0xbd56a23cd3221cb9d25029e80cd9f2afe2c615ae9c0b3956bf6ed373b8b805b6"
......
......@@ -1031,6 +1031,30 @@
"length": 41,
"filename_relative": "src/universal/CrossDomainMessenger.sol"
},
{
"id": "7d4963a32dc1dafcf8cfac2d7ac767f3cf4a2ee7ffa0cbd4ff7e6dbdf4d47727",
"impact": "Medium",
"confidence": "Medium",
"check": "tx-origin",
"description": "OptimismPortal2.finalizeWithdrawalTransaction(Types.WithdrawalTransaction) (src/L1/OptimismPortal2.sol#308-390) uses tx.origin for authorization: ! success && tx.origin == Constants.ESTIMATION_ADDRESS (src/L1/OptimismPortal2.sol#387)\n",
"type": "function",
"name": "finalizeWithdrawalTransaction",
"start": 14716,
"length": 4789,
"filename_relative": "src/L1/OptimismPortal2.sol"
},
{
"id": "7d4963a32dc1dafcf8cfac2d7ac767f3cf4a2ee7ffa0cbd4ff7e6dbdf4d47727",
"impact": "Medium",
"confidence": "Medium",
"check": "tx-origin",
"description": "OptimismPortal2.finalizeWithdrawalTransaction(Types.WithdrawalTransaction) (src/L1/OptimismPortal2.sol#308-390) uses tx.origin for authorization: ! success && tx.origin == Constants.ESTIMATION_ADDRESS (src/L1/OptimismPortal2.sol#387)\n",
"type": "node",
"name": "! success && tx.origin == Constants.ESTIMATION_ADDRESS",
"start": 19376,
"length": 53,
"filename_relative": "src/L1/OptimismPortal2.sol"
},
{
"id": "b507540468fdf3f9972d33e723efb8c120f8d824ffe54842e1e998244944abaf",
"impact": "Medium",
......@@ -1139,6 +1163,42 @@
"length": 20,
"filename_relative": "src/dispute/FaultDisputeGame.sol"
},
{
"id": "7dadbc3ea20cc8f9e628681fed9b1d804359f2f8e3ec7b768e65a39d72eb2db7",
"impact": "Medium",
"confidence": "High",
"check": "write-after-write",
"description": "OptimismPortal2.l2Sender (src/L1/OptimismPortal2.sol#54) is written in both\n\tl2Sender = _tx.sender (src/L1/OptimismPortal2.sol#366)\n\tl2Sender = Constants.DEFAULT_L2_SENDER (src/L1/OptimismPortal2.sol#378)\n",
"type": "variable",
"name": "l2Sender",
"start": 2796,
"length": 23,
"filename_relative": "src/L1/OptimismPortal2.sol"
},
{
"id": "7dadbc3ea20cc8f9e628681fed9b1d804359f2f8e3ec7b768e65a39d72eb2db7",
"impact": "Medium",
"confidence": "High",
"check": "write-after-write",
"description": "OptimismPortal2.l2Sender (src/L1/OptimismPortal2.sol#54) is written in both\n\tl2Sender = _tx.sender (src/L1/OptimismPortal2.sol#366)\n\tl2Sender = Constants.DEFAULT_L2_SENDER (src/L1/OptimismPortal2.sol#378)\n",
"type": "node",
"name": "l2Sender = _tx.sender",
"start": 18077,
"length": 21,
"filename_relative": "src/L1/OptimismPortal2.sol"
},
{
"id": "7dadbc3ea20cc8f9e628681fed9b1d804359f2f8e3ec7b768e65a39d72eb2db7",
"impact": "Medium",
"confidence": "High",
"check": "write-after-write",
"description": "OptimismPortal2.l2Sender (src/L1/OptimismPortal2.sol#54) is written in both\n\tl2Sender = _tx.sender (src/L1/OptimismPortal2.sol#366)\n\tl2Sender = Constants.DEFAULT_L2_SENDER (src/L1/OptimismPortal2.sol#378)\n",
"type": "node",
"name": "l2Sender = Constants.DEFAULT_L2_SENDER",
"start": 18871,
"length": 38,
"filename_relative": "src/L1/OptimismPortal2.sol"
},
{
"id": "858b95108475bbb08343b6f5701b03c0762d00b73cec53751173e93c3ec10c1a",
"impact": "Medium",
......
[
{
"inputs": [
{
"internalType": "uint256",
"name": "_proofMaturityDelaySeconds",
"type": "uint256"
},
{
"internalType": "uint256",
"name": "_disputeGameFinalityDelaySeconds",
"type": "uint256"
},
{
"internalType": "GameType",
"name": "_initialRespectedGameType",
"type": "uint32"
}
],
"stateMutability": "nonpayable",
"type": "constructor"
},
{
"stateMutability": "payable",
"type": "receive"
},
{
"inputs": [],
"name": "GUARDIAN",
"outputs": [
{
"internalType": "address",
"name": "",
"type": "address"
}
],
"stateMutability": "view",
"type": "function"
},
{
"inputs": [],
"name": "SYSTEM_CONFIG",
"outputs": [
{
"internalType": "contract SystemConfig",
"name": "",
"type": "address"
}
],
"stateMutability": "view",
"type": "function"
},
{
"inputs": [
{
"internalType": "contract IDisputeGame",
"name": "_disputeGame",
"type": "address"
}
],
"name": "blacklistDisputeGame",
"outputs": [],
"stateMutability": "nonpayable",
"type": "function"
},
{
"inputs": [
{
"internalType": "bytes32",
"name": "_withdrawalHash",
"type": "bytes32"
}
],
"name": "deleteProvenWithdrawal",
"outputs": [],
"stateMutability": "nonpayable",
"type": "function"
},
{
"inputs": [
{
"internalType": "address",
"name": "_to",
"type": "address"
},
{
"internalType": "uint256",
"name": "_value",
"type": "uint256"
},
{
"internalType": "uint64",
"name": "_gasLimit",
"type": "uint64"
},
{
"internalType": "bool",
"name": "_isCreation",
"type": "bool"
},
{
"internalType": "bytes",
"name": "_data",
"type": "bytes"
}
],
"name": "depositTransaction",
"outputs": [],
"stateMutability": "payable",
"type": "function"
},
{
"inputs": [
{
"internalType": "contract IDisputeGame",
"name": "",
"type": "address"
}
],
"name": "disputeGameBlacklist",
"outputs": [
{
"internalType": "bool",
"name": "",
"type": "bool"
}
],
"stateMutability": "view",
"type": "function"
},
{
"inputs": [],
"name": "disputeGameFactory",
"outputs": [
{
"internalType": "contract DisputeGameFactory",
"name": "",
"type": "address"
}
],
"stateMutability": "view",
"type": "function"
},
{
"inputs": [],
"name": "donateETH",
"outputs": [],
"stateMutability": "payable",
"type": "function"
},
{
"inputs": [
{
"components": [
{
"internalType": "uint256",
"name": "nonce",
"type": "uint256"
},
{
"internalType": "address",
"name": "sender",
"type": "address"
},
{
"internalType": "address",
"name": "target",
"type": "address"
},
{
"internalType": "uint256",
"name": "value",
"type": "uint256"
},
{
"internalType": "uint256",
"name": "gasLimit",
"type": "uint256"
},
{
"internalType": "bytes",
"name": "data",
"type": "bytes"
}
],
"internalType": "struct Types.WithdrawalTransaction",
"name": "_tx",
"type": "tuple"
}
],
"name": "finalizeWithdrawalTransaction",
"outputs": [],
"stateMutability": "nonpayable",
"type": "function"
},
{
"inputs": [
{
"internalType": "bytes32",
"name": "",
"type": "bytes32"
}
],
"name": "finalizedWithdrawals",
"outputs": [
{
"internalType": "bool",
"name": "",
"type": "bool"
}
],
"stateMutability": "view",
"type": "function"
},
{
"inputs": [],
"name": "guardian",
"outputs": [
{
"internalType": "address",
"name": "",
"type": "address"
}
],
"stateMutability": "view",
"type": "function"
},
{
"inputs": [
{
"internalType": "contract DisputeGameFactory",
"name": "_disputeGameFactory",
"type": "address"
},
{
"internalType": "contract SystemConfig",
"name": "_systemConfig",
"type": "address"
},
{
"internalType": "contract SuperchainConfig",
"name": "_superchainConfig",
"type": "address"
}
],
"name": "initialize",
"outputs": [],
"stateMutability": "nonpayable",
"type": "function"
},
{
"inputs": [],
"name": "l2Sender",
"outputs": [
{
"internalType": "address",
"name": "",
"type": "address"
}
],
"stateMutability": "view",
"type": "function"
},
{
"inputs": [
{
"internalType": "uint64",
"name": "_byteCount",
"type": "uint64"
}
],
"name": "minimumGasLimit",
"outputs": [
{
"internalType": "uint64",
"name": "",
"type": "uint64"
}
],
"stateMutability": "pure",
"type": "function"
},
{
"inputs": [],
"name": "params",
"outputs": [
{
"internalType": "uint128",
"name": "prevBaseFee",
"type": "uint128"
},
{
"internalType": "uint64",
"name": "prevBoughtGas",
"type": "uint64"
},
{
"internalType": "uint64",
"name": "prevBlockNum",
"type": "uint64"
}
],
"stateMutability": "view",
"type": "function"
},
{
"inputs": [],
"name": "paused",
"outputs": [
{
"internalType": "bool",
"name": "paused_",
"type": "bool"
}
],
"stateMutability": "view",
"type": "function"
},
{
"inputs": [
{
"components": [
{
"internalType": "uint256",
"name": "nonce",
"type": "uint256"
},
{
"internalType": "address",
"name": "sender",
"type": "address"
},
{
"internalType": "address",
"name": "target",
"type": "address"
},
{
"internalType": "uint256",
"name": "value",
"type": "uint256"
},
{
"internalType": "uint256",
"name": "gasLimit",
"type": "uint256"
},
{
"internalType": "bytes",
"name": "data",
"type": "bytes"
}
],
"internalType": "struct Types.WithdrawalTransaction",
"name": "_tx",
"type": "tuple"
},
{
"internalType": "uint256",
"name": "_disputeGameIndex",
"type": "uint256"
},
{
"components": [
{
"internalType": "bytes32",
"name": "version",
"type": "bytes32"
},
{
"internalType": "bytes32",
"name": "stateRoot",
"type": "bytes32"
},
{
"internalType": "bytes32",
"name": "messagePasserStorageRoot",
"type": "bytes32"
},
{
"internalType": "bytes32",
"name": "latestBlockhash",
"type": "bytes32"
}
],
"internalType": "struct Types.OutputRootProof",
"name": "_outputRootProof",
"type": "tuple"
},
{
"internalType": "bytes[]",
"name": "_withdrawalProof",
"type": "bytes[]"
}
],
"name": "proveWithdrawalTransaction",
"outputs": [],
"stateMutability": "nonpayable",
"type": "function"
},
{
"inputs": [
{
"internalType": "bytes32",
"name": "",
"type": "bytes32"
}
],
"name": "provenWithdrawals",
"outputs": [
{
"internalType": "contract IDisputeGame",
"name": "disputeGameProxy",
"type": "address"
},
{
"internalType": "uint64",
"name": "timestamp",
"type": "uint64"
}
],
"stateMutability": "view",
"type": "function"
},
{
"inputs": [],
"name": "respectedGameType",
"outputs": [
{
"internalType": "GameType",
"name": "",
"type": "uint32"
}
],
"stateMutability": "view",
"type": "function"
},
{
"inputs": [
{
"internalType": "GameType",
"name": "_gameType",
"type": "uint32"
}
],
"name": "setRespectedGameType",
"outputs": [],
"stateMutability": "nonpayable",
"type": "function"
},
{
"inputs": [],
"name": "superchainConfig",
"outputs": [
{
"internalType": "contract SuperchainConfig",
"name": "",
"type": "address"
}
],
"stateMutability": "view",
"type": "function"
},
{
"inputs": [],
"name": "systemConfig",
"outputs": [
{
"internalType": "contract SystemConfig",
"name": "",
"type": "address"
}
],
"stateMutability": "view",
"type": "function"
},
{
"inputs": [],
"name": "version",
"outputs": [
{
"internalType": "string",
"name": "",
"type": "string"
}
],
"stateMutability": "view",
"type": "function"
},
{
"anonymous": false,
"inputs": [
{
"indexed": false,
"internalType": "uint8",
"name": "version",
"type": "uint8"
}
],
"name": "Initialized",
"type": "event"
},
{
"anonymous": false,
"inputs": [
{
"indexed": true,
"internalType": "address",
"name": "from",
"type": "address"
},
{
"indexed": true,
"internalType": "address",
"name": "to",
"type": "address"
},
{
"indexed": true,
"internalType": "uint256",
"name": "version",
"type": "uint256"
},
{
"indexed": false,
"internalType": "bytes",
"name": "opaqueData",
"type": "bytes"
}
],
"name": "TransactionDeposited",
"type": "event"
},
{
"anonymous": false,
"inputs": [
{
"indexed": true,
"internalType": "bytes32",
"name": "withdrawalHash",
"type": "bytes32"
},
{
"indexed": false,
"internalType": "bool",
"name": "success",
"type": "bool"
}
],
"name": "WithdrawalFinalized",
"type": "event"
},
{
"anonymous": false,
"inputs": [
{
"indexed": true,
"internalType": "bytes32",
"name": "withdrawalHash",
"type": "bytes32"
},
{
"indexed": true,
"internalType": "address",
"name": "from",
"type": "address"
},
{
"indexed": true,
"internalType": "address",
"name": "to",
"type": "address"
}
],
"name": "WithdrawalProven",
"type": "event"
}
]
\ No newline at end of file
[
{
"bytes": "1",
"label": "_initialized",
"offset": 0,
"slot": "0",
"type": "uint8"
},
{
"bytes": "1",
"label": "_initializing",
"offset": 1,
"slot": "0",
"type": "bool"
},
{
"bytes": "32",
"label": "params",
"offset": 0,
"slot": "1",
"type": "struct ResourceMetering.ResourceParams"
},
{
"bytes": "1536",
"label": "__gap",
"offset": 0,
"slot": "2",
"type": "uint256[48]"
},
{
"bytes": "20",
"label": "l2Sender",
"offset": 0,
"slot": "50",
"type": "address"
},
{
"bytes": "32",
"label": "finalizedWithdrawals",
"offset": 0,
"slot": "51",
"type": "mapping(bytes32 => bool)"
},
{
"bytes": "32",
"label": "spacer_52_0_32",
"offset": 0,
"slot": "52",
"type": "bytes32"
},
{
"bytes": "1",
"label": "spacer_53_0_1",
"offset": 0,
"slot": "53",
"type": "bool"
},
{
"bytes": "20",
"label": "superchainConfig",
"offset": 1,
"slot": "53",
"type": "contract SuperchainConfig"
},
{
"bytes": "20",
"label": "spacer_54_0_20",
"offset": 0,
"slot": "54",
"type": "address"
},
{
"bytes": "20",
"label": "systemConfig",
"offset": 0,
"slot": "55",
"type": "contract SystemConfig"
},
{
"bytes": "20",
"label": "disputeGameFactory",
"offset": 0,
"slot": "56",
"type": "contract DisputeGameFactory"
},
{
"bytes": "32",
"label": "provenWithdrawals",
"offset": 0,
"slot": "57",
"type": "mapping(bytes32 => struct OptimismPortal2.ProvenWithdrawal)"
},
{
"bytes": "32",
"label": "disputeGameBlacklist",
"offset": 0,
"slot": "58",
"type": "mapping(contract IDisputeGame => bool)"
},
{
"bytes": "4",
"label": "respectedGameType",
"offset": 0,
"slot": "59",
"type": "GameType"
}
]
\ No newline at end of file
// SPDX-License-Identifier: MIT
pragma solidity 0.8.15;
import { Initializable } from "@openzeppelin/contracts/proxy/utils/Initializable.sol";
import { SafeCall } from "src/libraries/SafeCall.sol";
import { DisputeGameFactory, IDisputeGame } from "src/dispute/DisputeGameFactory.sol";
import { SystemConfig } from "src/L1/SystemConfig.sol";
import { SuperchainConfig } from "src/L1/SuperchainConfig.sol";
import { Constants } from "src/libraries/Constants.sol";
import { Types } from "src/libraries/Types.sol";
import { Hashing } from "src/libraries/Hashing.sol";
import { SecureMerkleTrie } from "src/libraries/trie/SecureMerkleTrie.sol";
import { AddressAliasHelper } from "src/vendor/AddressAliasHelper.sol";
import { ResourceMetering } from "src/L1/ResourceMetering.sol";
import { ISemver } from "src/universal/ISemver.sol";
import { Constants } from "src/libraries/Constants.sol";
import "src/libraries/DisputeTypes.sol";
/// @custom:proxied
/// @title OptimismPortal2
/// @notice The OptimismPortal is a low-level contract responsible for passing messages between L1
/// and L2. Messages sent directly to the OptimismPortal have no form of replayability.
/// Users are encouraged to use the L1CrossDomainMessenger for a higher-level interface.
contract OptimismPortal2 is Initializable, ResourceMetering, ISemver {
/// @notice Represents a proven withdrawal.
/// @custom:field disputeGameProxy The address of the dispute game proxy that the withdrawal was proven against.
/// @custom:field timestamp Timestamp at whcih the withdrawal was proven.
struct ProvenWithdrawal {
IDisputeGame disputeGameProxy;
uint64 timestamp;
}
/// @dev Remove this in favor of a configurable sauron role. This should probably live in the superchain config,
/// but need to confirm with security.
address internal constant SAURON = address(0xdead);
/// @notice The delay between when a withdrawal transaction is proven and when it may be finalized.
uint256 internal immutable PROOF_MATURITY_DELAY_SECONDS;
/// @notice The delay between when a dispute game is resolved and when a withdrawal proven against it may be
/// finalized.
uint256 internal immutable DISPUTE_GAME_FINALITY_DELAY_SECONDS;
/// @notice Version of the deposit event.
uint256 internal constant DEPOSIT_VERSION = 0;
/// @notice The L2 gas limit set when eth is deposited using the receive() function.
uint64 internal constant RECEIVE_DEFAULT_GAS_LIMIT = 100_000;
/// @notice Address of the L2 account which initiated a withdrawal in this transaction.
/// If the of this variable is the default L2 sender address, then we are NOT inside of
/// a call to finalizeWithdrawalTransaction.
address public l2Sender;
/// @notice A list of withdrawal hashes which have been successfully finalized.
mapping(bytes32 => bool) public finalizedWithdrawals;
/// @custom:legacy
/// @custom:spacer provenWithdrawals
/// @notice Spacer taking up the legacy `provenWithdrawals` mapping slot.
bytes32 private spacer_52_0_32;
/// @custom:legacy
/// @custom:spacer paused
/// @notice Spacer for backwards compatibility.
bool private spacer_53_0_1;
/// @notice Contract of the Superchain Config.
SuperchainConfig public superchainConfig;
/// @custom:legacy
/// @custom:spacer l2Oracle
/// @notice Spacer taking up the legacy `l2Oracle` address slot.
address private spacer_54_0_20;
/// @notice Contract of the SystemConfig.
/// @custom:network-specific
SystemConfig public systemConfig;
/// @notice Address of the DisputeGameFactory.
/// @custom:network-specific
DisputeGameFactory public disputeGameFactory;
/// @notice A mapping of withdrawal hashes to `ProvenWithdrawal` data.
mapping(bytes32 => ProvenWithdrawal) public provenWithdrawals;
/// @notice A mapping of dispute game addresses to whether or not they are blacklisted.
mapping(IDisputeGame => bool) public disputeGameBlacklist;
/// @notice The game type that the OptimismPortal consults for output proposals.
GameType public respectedGameType;
/// @notice Emitted when a transaction is deposited from L1 to L2.
/// The parameters of this event are read by the rollup node and used to derive deposit
/// transactions on L2.
/// @param from Address that triggered the deposit transaction.
/// @param to Address that the deposit transaction is directed to.
/// @param version Version of this deposit transaction event.
/// @param opaqueData ABI encoded deposit data to be parsed off-chain.
event TransactionDeposited(address indexed from, address indexed to, uint256 indexed version, bytes opaqueData);
/// @notice Emitted when a withdrawal transaction is proven.
/// @param withdrawalHash Hash of the withdrawal transaction.
/// @param from Address that triggered the withdrawal transaction.
/// @param to Address that the withdrawal transaction is directed to.
event WithdrawalProven(bytes32 indexed withdrawalHash, address indexed from, address indexed to);
/// @notice Emitted when a withdrawal transaction is finalized.
/// @param withdrawalHash Hash of the withdrawal transaction.
/// @param success Whether the withdrawal transaction was successful.
event WithdrawalFinalized(bytes32 indexed withdrawalHash, bool success);
/// @notice Reverts when paused.
modifier whenNotPaused() {
require(!paused(), "OptimismPortal: paused");
_;
}
/// @notice Semantic version.
/// @custom:semver 3.0.0
string public constant version = "3.0.0";
/// @notice Constructs the OptimismPortal contract.
constructor(
uint256 _proofMaturityDelaySeconds,
uint256 _disputeGameFinalityDelaySeconds,
GameType _initialRespectedGameType
) {
PROOF_MATURITY_DELAY_SECONDS = _proofMaturityDelaySeconds;
DISPUTE_GAME_FINALITY_DELAY_SECONDS = _disputeGameFinalityDelaySeconds;
respectedGameType = _initialRespectedGameType;
initialize({
_disputeGameFactory: DisputeGameFactory(address(0)),
_systemConfig: SystemConfig(address(0)),
_superchainConfig: SuperchainConfig(address(0))
});
}
/// @notice Initializer.
/// @param _disputeGameFactory Contract of the DisputeGameFactory.
/// @param _systemConfig Contract of the SystemConfig.
/// @param _superchainConfig Contract of the SuperchainConfig.
function initialize(
DisputeGameFactory _disputeGameFactory,
SystemConfig _systemConfig,
SuperchainConfig _superchainConfig
)
public
initializer
{
disputeGameFactory = _disputeGameFactory;
systemConfig = _systemConfig;
superchainConfig = _superchainConfig;
if (l2Sender == address(0)) {
l2Sender = Constants.DEFAULT_L2_SENDER;
}
__ResourceMetering_init();
}
/// @notice Getter function for the contract of the SystemConfig on this chain.
/// Public getter is legacy and will be removed in the future. Use `systemConfig()` instead.
/// @return Contract of the SystemConfig on this chain.
/// @custom:legacy
function SYSTEM_CONFIG() external view returns (SystemConfig) {
return systemConfig;
}
/// @notice Getter function for the address of the guardian.
/// Public getter is legacy and will be removed in the future. Use `SuperchainConfig.guardian()` instead.
/// @return Address of the guardian.
/// @custom:legacy
function GUARDIAN() external view returns (address) {
return guardian();
}
/// @notice Getter function for the address of the guardian.
/// Public getter is legacy and will be removed in the future. Use `SuperchainConfig.guardian()` instead.
/// @return Address of the guardian.
/// @custom:legacy
function guardian() public view returns (address) {
return superchainConfig.guardian();
}
/// @notice Getter for the current paused status.
/// @return paused_ Whether or not the contract is paused.
function paused() public view returns (bool paused_) {
paused_ = superchainConfig.paused();
}
/// @notice Computes the minimum gas limit for a deposit.
/// The minimum gas limit linearly increases based on the size of the calldata.
/// This is to prevent users from creating L2 resource usage without paying for it.
/// This function can be used when interacting with the portal to ensure forwards
/// compatibility.
/// @param _byteCount Number of bytes in the calldata.
/// @return The minimum gas limit for a deposit.
function minimumGasLimit(uint64 _byteCount) public pure returns (uint64) {
return _byteCount * 16 + 21000;
}
/// @notice Accepts value so that users can send ETH directly to this contract and have the
/// funds be deposited to their address on L2. This is intended as a convenience
/// function for EOAs. Contracts should call the depositTransaction() function directly
/// otherwise any deposited funds will be lost due to address aliasing.
// solhint-disable-next-line ordering
receive() external payable {
depositTransaction(msg.sender, msg.value, RECEIVE_DEFAULT_GAS_LIMIT, false, bytes(""));
}
/// @notice Accepts ETH value without triggering a deposit to L2.
/// This function mainly exists for the sake of the migration between the legacy
/// Optimism system and Bedrock.
function donateETH() external payable {
// Intentionally empty.
}
/// @notice Getter for the resource config.
/// Used internally by the ResourceMetering contract.
/// The SystemConfig is the source of truth for the resource config.
/// @return ResourceMetering ResourceConfig
function _resourceConfig() internal view override returns (ResourceMetering.ResourceConfig memory) {
return systemConfig.resourceConfig();
}
/// @notice Proves a withdrawal transaction.
/// @param _tx Withdrawal transaction to finalize.
/// @param _disputeGameIndex Index of the dispute game to prove the withdrawal against.
/// @param _outputRootProof Inclusion proof of the L2ToL1MessagePasser contract's storage root.
/// @param _withdrawalProof Inclusion proof of the withdrawal in L2ToL1MessagePasser contract.
function proveWithdrawalTransaction(
Types.WithdrawalTransaction memory _tx,
uint256 _disputeGameIndex,
Types.OutputRootProof calldata _outputRootProof,
bytes[] calldata _withdrawalProof
)
external
whenNotPaused
{
// Prevent users from creating a deposit transaction where this address is the message
// sender on L2. Because this is checked here, we do not need to check again in
// `finalizeWithdrawalTransaction`.
require(_tx.target != address(this), "OptimismPortal: you cannot send messages to the portal contract");
// Fetch the dispute game proxy from the `DisputeGameFactory` contract.
(GameType gameType,, IDisputeGame gameProxy) = disputeGameFactory.gameAtIndex(_disputeGameIndex);
Claim outputRoot = gameProxy.rootClaim();
// The game type of the dispute game must be the respected game type.
require(gameType.raw() == respectedGameType.raw(), "OptimismPortal: invalid game type");
// Verify that the output root can be generated with the elements in the proof.
require(
outputRoot.raw() == Hashing.hashOutputRootProof(_outputRootProof),
"OptimismPortal: invalid output root proof"
);
// Load the ProvenWithdrawal into memory, using the withdrawal hash as a unique identifier.
bytes32 withdrawalHash = Hashing.hashWithdrawal(_tx);
ProvenWithdrawal memory provenWithdrawal = provenWithdrawals[withdrawalHash];
// We generally want to prevent users from proving the same withdrawal multiple times
// because each successive proof will update the timestamp. A malicious user can take
// advantage of this to prevent other users from finalizing their withdrawal. However,
// in the case that an honest user proves their withdrawal against a dispute game that
// resolves against the root claim, or the dispute game is blacklisted, we allow
// re-proving the withdrawal against a new proposal.
require(
provenWithdrawal.timestamp == 0 || gameProxy.status() == GameStatus.CHALLENGER_WINS
|| disputeGameBlacklist[gameProxy],
"OptimismPortal: withdrawal hash has already been proven, and dispute game is not invalid"
);
// Compute the storage slot of the withdrawal hash in the L2ToL1MessagePasser contract.
// Refer to the Solidity documentation for more information on how storage layouts are
// computed for mappings.
bytes32 storageKey = keccak256(
abi.encode(
withdrawalHash,
uint256(0) // The withdrawals mapping is at the first slot in the layout.
)
);
// Verify that the hash of this withdrawal was stored in the L2toL1MessagePasser contract
// on L2. If this is true, under the assumption that the SecureMerkleTrie does not have
// bugs, then we know that this withdrawal was actually triggered on L2 and can therefore
// be relayed on L1.
require(
SecureMerkleTrie.verifyInclusionProof(
abi.encode(storageKey), hex"01", _withdrawalProof, _outputRootProof.messagePasserStorageRoot
),
"OptimismPortal: invalid withdrawal inclusion proof"
);
// Designate the withdrawalHash as proven by storing the `disputeGameProxy` & `timestamp` in the
// `provenWithdrawals` mapping. A `withdrawalHash` can only be proven once unless the dispute game it proved
// against resolves against the favor of the root claim.
provenWithdrawals[withdrawalHash] =
ProvenWithdrawal({ disputeGameProxy: gameProxy, timestamp: uint64(block.timestamp) });
// Emit a `WithdrawalProven` event.
emit WithdrawalProven(withdrawalHash, _tx.sender, _tx.target);
}
/// @notice Finalizes a withdrawal transaction.
/// @param _tx Withdrawal transaction to finalize.
function finalizeWithdrawalTransaction(Types.WithdrawalTransaction memory _tx) external whenNotPaused {
// Make sure that the l2Sender has not yet been set. The l2Sender is set to a value other
// than the default value when a withdrawal transaction is being finalized. This check is
// a defacto reentrancy guard.
require(
l2Sender == Constants.DEFAULT_L2_SENDER, "OptimismPortal: can only trigger one withdrawal per transaction"
);
// Grab the proven withdrawal from the `provenWithdrawals` map.
bytes32 withdrawalHash = Hashing.hashWithdrawal(_tx);
ProvenWithdrawal memory provenWithdrawal = provenWithdrawals[withdrawalHash];
IDisputeGame disputeGameProxy = provenWithdrawal.disputeGameProxy;
// The dispute game must not be blacklisted.
require(!disputeGameBlacklist[disputeGameProxy], "OptimismPortal: dispute game has been blacklisted");
// A withdrawal can only be finalized if it has been proven. We know that a withdrawal has
// been proven at least once when its timestamp is non-zero. Unproven withdrawals will have
// a timestamp of zero.
require(provenWithdrawal.timestamp != 0, "OptimismPortal: withdrawal has not been proven yet");
// As a sanity check, we make sure that the proven withdrawal's timestamp is greater than
// starting timestamp inside the Dispute Game. Not strictly necessary but extra layer of
// safety against weird bugs in the proving step.
require(
provenWithdrawal.timestamp > disputeGameProxy.createdAt().raw(),
"OptimismPortal: withdrawal timestamp less than L2 Oracle starting timestamp"
);
// A proven withdrawal must wait at least `PROOF_MATURITY_DELAY_SECONDS` before finalizing.
require(
block.timestamp - provenWithdrawal.timestamp > PROOF_MATURITY_DELAY_SECONDS,
"OptimismPortal: proven withdrawal has not matured yet"
);
// A proven withdrawal must wait until the dispute game it was proven against has been
// resolved in favor of the root claim (the output proposal). This is to prevent users
// from finalizing withdrawals proven against non-finalized output roots.
require(
disputeGameProxy.status() == GameStatus.DEFENDER_WINS,
"OptimismPortal: output proposal has not been finalized yet"
);
// Before a withdrawal can be finalized, the dispute game it was proven against must have been
// resolved for at least `DISPUTE_GAME_FINALITY_DELAY_SECONDS`. This is to allow for manual
// intervention in the event that a dispute game is resolved incorrectly.
require(
block.timestamp - disputeGameProxy.resolvedAt().raw() > DISPUTE_GAME_FINALITY_DELAY_SECONDS,
"OptimismPortal: output proposal in air-gap"
);
// Check that this withdrawal has not already been finalized, this is replay protection.
require(!finalizedWithdrawals[withdrawalHash], "OptimismPortal: withdrawal has already been finalized");
// Mark the withdrawal as finalized so it can't be replayed.
finalizedWithdrawals[withdrawalHash] = true;
// Set the l2Sender so contracts know who triggered this withdrawal on L2.
l2Sender = _tx.sender;
// Trigger the call to the target contract. We use a custom low level method
// SafeCall.callWithMinGas to ensure two key properties
// 1. Target contracts cannot force this call to run out of gas by returning a very large
// amount of data (and this is OK because we don't care about the returndata here).
// 2. The amount of gas provided to the execution context of the target is at least the
// gas limit specified by the user. If there is not enough gas in the current context
// to accomplish this, `callWithMinGas` will revert.
bool success = SafeCall.callWithMinGas(_tx.target, _tx.gasLimit, _tx.value, _tx.data);
// Reset the l2Sender back to the default value.
l2Sender = Constants.DEFAULT_L2_SENDER;
// All withdrawals are immediately finalized. Replayability can
// be achieved through contracts built on top of this contract
emit WithdrawalFinalized(withdrawalHash, success);
// Reverting here is useful for determining the exact gas cost to successfully execute the
// sub call to the target contract if the minimum gas limit specified by the user would not
// be sufficient to execute the sub call.
if (!success && tx.origin == Constants.ESTIMATION_ADDRESS) {
revert("OptimismPortal: withdrawal failed");
}
}
/// @notice Accepts deposits of ETH and data, and emits a TransactionDeposited event for use in
/// deriving deposit transactions. Note that if a deposit is made by a contract, its
/// address will be aliased when retrieved using `tx.origin` or `msg.sender`. Consider
/// using the CrossDomainMessenger contracts for a simpler developer experience.
/// @param _to Target address on L2.
/// @param _value ETH value to send to the recipient.
/// @param _gasLimit Amount of L2 gas to purchase by burning gas on L1.
/// @param _isCreation Whether or not the transaction is a contract creation.
/// @param _data Data to trigger the recipient with.
function depositTransaction(
address _to,
uint256 _value,
uint64 _gasLimit,
bool _isCreation,
bytes memory _data
)
public
payable
metered(_gasLimit)
{
// Just to be safe, make sure that people specify address(0) as the target when doing
// contract creations.
if (_isCreation) {
require(_to == address(0), "OptimismPortal: must send to address(0) when creating a contract");
}
// Prevent depositing transactions that have too small of a gas limit. Users should pay
// more for more resource usage.
require(_gasLimit >= minimumGasLimit(uint64(_data.length)), "OptimismPortal: gas limit too small");
// Prevent the creation of deposit transactions that have too much calldata. This gives an
// upper limit on the size of unsafe blocks over the p2p network. 120kb is chosen to ensure
// that the transaction can fit into the p2p network policy of 128kb even though deposit
// transactions are not gossipped over the p2p network.
require(_data.length <= 120_000, "OptimismPortal: data too large");
// Transform the from-address to its alias if the caller is a contract.
address from = msg.sender;
if (msg.sender != tx.origin) {
from = AddressAliasHelper.applyL1ToL2Alias(msg.sender);
}
// Compute the opaque data that will be emitted as part of the TransactionDeposited event.
// We use opaque data so that we can update the TransactionDeposited event in the future
// without breaking the current interface.
bytes memory opaqueData = abi.encodePacked(msg.value, _value, _gasLimit, _isCreation, _data);
// Emit a TransactionDeposited event so that the rollup node can derive a deposit
// transaction for this deposit.
emit TransactionDeposited(from, _to, DEPOSIT_VERSION, opaqueData);
}
/// @notice Blacklists a dispute game. Should only be used in the event that a dispute game resolves incorrectly.
/// @param _disputeGame Dispute game to blacklist.
function blacklistDisputeGame(IDisputeGame _disputeGame) external {
require(msg.sender == SAURON, "OptimismPortal: only sauron can blacklist dispute games");
disputeGameBlacklist[_disputeGame] = true;
}
/// @notice Deletes a proven withdrawal from the `provenWithdrawals` mapping in the case that a MPT proof was
/// incorrectly verified by the `MerkleTrie` verifier contract.
/// @param _withdrawalHash Hash of the withdrawal transaction to delete from the `pendingWithdrawals` mapping.
function deleteProvenWithdrawal(bytes32 _withdrawalHash) external {
require(msg.sender == SAURON, "OptimismPortal: only sauron can delete proven withdrawals");
delete provenWithdrawals[_withdrawalHash];
}
/// @notice Sets the respected game type. Changing this value can alter the security properties of the system,
/// depending on the new game's behavior.
/// @param _gameType The game type to consult for output proposals.
function setRespectedGameType(GameType _gameType) external {
require(msg.sender == SAURON, "OptimismPortal: only sauron can set the respected game type");
respectedGameType = _gameType;
}
}
......@@ -16,6 +16,10 @@ interface IDisputeGame is IInitializable {
/// @return createdAt_ The timestamp that the DisputeGame contract was created at.
function createdAt() external view returns (Timestamp createdAt_);
/// @notice Returns the timestamp that the DisputeGame contract was resolved at.
/// @return resolvedAt_ The timestamp that the DisputeGame contract was resolved at.
function resolvedAt() external view returns (Timestamp resolvedAt_);
/// @notice Returns the current status of the game.
/// @return status_ The current status of the game.
function status() external view returns (GameStatus status_);
......
......@@ -6,6 +6,7 @@ import { Executables } from "scripts/Executables.sol";
import { console2 as console } from "forge-std/console2.sol";
import { ProtocolVersions } from "src/L1/ProtocolVersions.sol";
import { OptimismPortal } from "src/L1/OptimismPortal.sol";
import { OptimismPortal2 } from "src/L1/OptimismPortal2.sol";
import { SystemConfig } from "src/L1/SystemConfig.sol";
/// @title Specification_Test
......@@ -222,6 +223,35 @@ contract Specification_Test is CommonTest {
_addSpec({ _name: "OptimismPortal", _sel: _getSel("systemConfig()") });
_addSpec({ _name: "OptimismPortal", _sel: _getSel("version()") });
// OptimismPortal2
_addSpec({ _name: "OptimismPortal2", _sel: _getSel("GUARDIAN()") });
_addSpec({ _name: "OptimismPortal2", _sel: _getSel("SYSTEM_CONFIG()") });
_addSpec({ _name: "OptimismPortal2", _sel: _getSel("depositTransaction(address,uint256,uint64,bool,bytes)") });
_addSpec({ _name: "OptimismPortal2", _sel: _getSel("donateETH()") });
_addSpec({
_name: "OptimismPortal2",
_sel: OptimismPortal2.finalizeWithdrawalTransaction.selector,
_pausable: true
});
_addSpec({ _name: "OptimismPortal2", _sel: _getSel("finalizedWithdrawals(bytes32)") });
_addSpec({ _name: "OptimismPortal2", _sel: _getSel("guardian()") });
_addSpec({ _name: "OptimismPortal2", _sel: _getSel("initialize(address,address,address)") });
_addSpec({ _name: "OptimismPortal2", _sel: _getSel("l2Sender()") });
_addSpec({ _name: "OptimismPortal2", _sel: _getSel("minimumGasLimit(uint64)") });
_addSpec({ _name: "OptimismPortal2", _sel: _getSel("params()") });
_addSpec({ _name: "OptimismPortal2", _sel: _getSel("paused()") });
_addSpec({ _name: "OptimismPortal2", _sel: OptimismPortal2.proveWithdrawalTransaction.selector, _pausable: true });
_addSpec({ _name: "OptimismPortal2", _sel: _getSel("provenWithdrawals(bytes32)") });
_addSpec({ _name: "OptimismPortal2", _sel: _getSel("superchainConfig()") });
_addSpec({ _name: "OptimismPortal2", _sel: _getSel("systemConfig()") });
_addSpec({ _name: "OptimismPortal2", _sel: _getSel("version()") });
_addSpec({ _name: "OptimismPortal2", _sel: _getSel("disputeGameFactory()") });
_addSpec({ _name: "OptimismPortal2", _sel: _getSel("disputeGameBlacklist(address)") });
_addSpec({ _name: "OptimismPortal2", _sel: _getSel("respectedGameType()") });
_addSpec({ _name: "OptimismPortal2", _sel: _getSel("blacklistDisputeGame(address)") });
_addSpec({ _name: "OptimismPortal2", _sel: _getSel("deleteProvenWithdrawal(bytes32)") });
_addSpec({ _name: "OptimismPortal2", _sel: _getSel("setRespectedGameType(uint32)") });
// ProtocolVersions
_addSpec({ _name: "ProtocolVersions", _sel: _getSel("RECOMMENDED_SLOT()") });
_addSpec({ _name: "ProtocolVersions", _sel: _getSel("REQUIRED_SLOT()") });
......
......@@ -271,8 +271,9 @@ contract Initializer_Test is Bridge_Initializer {
/// 3. The `initialize()` function of each contract cannot be called more than once.
function test_cannotReinitialize_succeeds() public {
// Ensure that all L1, L2 `Initializable` contracts are accounted for, in addition to
// OptimismMintableERC20FactoryImpl and OptimismMintableERC20FactoryProxy
assertEq(_getNumInitializable() + 2, contracts.length);
// OptimismMintableERC20FactoryImpl and OptimismMintableERC20FactoryProxy.
// TODO: Add `OptimismPortal2` and `OptimismPortal2Proxy` once the deployment scripts are updated.
assertEq(_getNumInitializable(), contracts.length);
// Attempt to re-initialize all contracts within the `contracts` array.
for (uint256 i; i < contracts.length; i++) {
......
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