github.com/ethereum-optimism/optimism@v1.7.2/packages/contracts-bedrock/src/L1/OptimismPortal2.sol (about) 1 // SPDX-License-Identifier: MIT 2 pragma solidity 0.8.15; 3 4 import { Initializable } from "@openzeppelin/contracts/proxy/utils/Initializable.sol"; 5 import { SafeCall } from "src/libraries/SafeCall.sol"; 6 import { DisputeGameFactory, IDisputeGame } from "src/dispute/DisputeGameFactory.sol"; 7 import { SystemConfig } from "src/L1/SystemConfig.sol"; 8 import { SuperchainConfig } from "src/L1/SuperchainConfig.sol"; 9 import { Constants } from "src/libraries/Constants.sol"; 10 import { Types } from "src/libraries/Types.sol"; 11 import { Hashing } from "src/libraries/Hashing.sol"; 12 import { SecureMerkleTrie } from "src/libraries/trie/SecureMerkleTrie.sol"; 13 import { AddressAliasHelper } from "src/vendor/AddressAliasHelper.sol"; 14 import { ResourceMetering } from "src/L1/ResourceMetering.sol"; 15 import { ISemver } from "src/universal/ISemver.sol"; 16 import { Constants } from "src/libraries/Constants.sol"; 17 18 import "src/libraries/DisputeTypes.sol"; 19 20 /// @custom:proxied 21 /// @title OptimismPortal2 22 /// @notice The OptimismPortal is a low-level contract responsible for passing messages between L1 23 /// and L2. Messages sent directly to the OptimismPortal have no form of replayability. 24 /// Users are encouraged to use the L1CrossDomainMessenger for a higher-level interface. 25 contract OptimismPortal2 is Initializable, ResourceMetering, ISemver { 26 /// @notice Represents a proven withdrawal. 27 /// @custom:field disputeGameProxy The address of the dispute game proxy that the withdrawal was proven against. 28 /// @custom:field timestamp Timestamp at whcih the withdrawal was proven. 29 struct ProvenWithdrawal { 30 IDisputeGame disputeGameProxy; 31 uint64 timestamp; 32 } 33 34 /// @notice The delay between when a withdrawal transaction is proven and when it may be finalized. 35 uint256 internal immutable PROOF_MATURITY_DELAY_SECONDS; 36 37 /// @notice The delay between when a dispute game is resolved and when a withdrawal proven against it may be 38 /// finalized. 39 uint256 internal immutable DISPUTE_GAME_FINALITY_DELAY_SECONDS; 40 41 /// @notice Version of the deposit event. 42 uint256 internal constant DEPOSIT_VERSION = 0; 43 44 /// @notice The L2 gas limit set when eth is deposited using the receive() function. 45 uint64 internal constant RECEIVE_DEFAULT_GAS_LIMIT = 100_000; 46 47 /// @notice Address of the L2 account which initiated a withdrawal in this transaction. 48 /// If the of this variable is the default L2 sender address, then we are NOT inside of 49 /// a call to finalizeWithdrawalTransaction. 50 address public l2Sender; 51 52 /// @notice A list of withdrawal hashes which have been successfully finalized. 53 mapping(bytes32 => bool) public finalizedWithdrawals; 54 55 /// @custom:legacy 56 /// @custom:spacer provenWithdrawals 57 /// @notice Spacer taking up the legacy `provenWithdrawals` mapping slot. 58 bytes32 private spacer_52_0_32; 59 60 /// @custom:legacy 61 /// @custom:spacer paused 62 /// @notice Spacer for backwards compatibility. 63 bool private spacer_53_0_1; 64 65 /// @notice Contract of the Superchain Config. 66 SuperchainConfig public superchainConfig; 67 68 /// @custom:legacy 69 /// @custom:spacer l2Oracle 70 /// @notice Spacer taking up the legacy `l2Oracle` address slot. 71 address private spacer_54_0_20; 72 73 /// @notice Contract of the SystemConfig. 74 /// @custom:network-specific 75 SystemConfig public systemConfig; 76 77 /// @notice Address of the DisputeGameFactory. 78 /// @custom:network-specific 79 DisputeGameFactory public disputeGameFactory; 80 81 /// @notice A mapping of withdrawal hashes to `ProvenWithdrawal` data. 82 mapping(bytes32 => ProvenWithdrawal) public provenWithdrawals; 83 84 /// @notice A mapping of dispute game addresses to whether or not they are blacklisted. 85 mapping(IDisputeGame => bool) public disputeGameBlacklist; 86 87 /// @notice The game type that the OptimismPortal consults for output proposals. 88 GameType public respectedGameType; 89 90 /// @notice The timestamp at which the respected game type was last updated. 91 uint64 public respectedGameTypeUpdatedAt; 92 93 /// @notice Emitted when a transaction is deposited from L1 to L2. 94 /// The parameters of this event are read by the rollup node and used to derive deposit 95 /// transactions on L2. 96 /// @param from Address that triggered the deposit transaction. 97 /// @param to Address that the deposit transaction is directed to. 98 /// @param version Version of this deposit transaction event. 99 /// @param opaqueData ABI encoded deposit data to be parsed off-chain. 100 event TransactionDeposited(address indexed from, address indexed to, uint256 indexed version, bytes opaqueData); 101 102 /// @notice Emitted when a withdrawal transaction is proven. 103 /// @param withdrawalHash Hash of the withdrawal transaction. 104 /// @param from Address that triggered the withdrawal transaction. 105 /// @param to Address that the withdrawal transaction is directed to. 106 event WithdrawalProven(bytes32 indexed withdrawalHash, address indexed from, address indexed to); 107 108 /// @notice Emitted when a withdrawal transaction is finalized. 109 /// @param withdrawalHash Hash of the withdrawal transaction. 110 /// @param success Whether the withdrawal transaction was successful. 111 event WithdrawalFinalized(bytes32 indexed withdrawalHash, bool success); 112 113 /// @notice Reverts when paused. 114 modifier whenNotPaused() { 115 require(!paused(), "OptimismPortal: paused"); 116 _; 117 } 118 119 /// @notice Semantic version. 120 /// @custom:semver 3.3.0 121 string public constant version = "3.3.0"; 122 123 /// @notice Constructs the OptimismPortal contract. 124 constructor( 125 uint256 _proofMaturityDelaySeconds, 126 uint256 _disputeGameFinalityDelaySeconds, 127 GameType _initialRespectedGameType 128 ) { 129 PROOF_MATURITY_DELAY_SECONDS = _proofMaturityDelaySeconds; 130 DISPUTE_GAME_FINALITY_DELAY_SECONDS = _disputeGameFinalityDelaySeconds; 131 respectedGameType = _initialRespectedGameType; 132 133 initialize({ 134 _disputeGameFactory: DisputeGameFactory(address(0)), 135 _systemConfig: SystemConfig(address(0)), 136 _superchainConfig: SuperchainConfig(address(0)) 137 }); 138 } 139 140 /// @notice Initializer. 141 /// @param _disputeGameFactory Contract of the DisputeGameFactory. 142 /// @param _systemConfig Contract of the SystemConfig. 143 /// @param _superchainConfig Contract of the SuperchainConfig. 144 function initialize( 145 DisputeGameFactory _disputeGameFactory, 146 SystemConfig _systemConfig, 147 SuperchainConfig _superchainConfig 148 ) 149 public 150 initializer 151 { 152 disputeGameFactory = _disputeGameFactory; 153 systemConfig = _systemConfig; 154 superchainConfig = _superchainConfig; 155 if (l2Sender == address(0)) { 156 l2Sender = Constants.DEFAULT_L2_SENDER; 157 } 158 __ResourceMetering_init(); 159 } 160 161 /// @notice Getter function for the contract of the SystemConfig on this chain. 162 /// Public getter is legacy and will be removed in the future. Use `systemConfig()` instead. 163 /// @return Contract of the SystemConfig on this chain. 164 /// @custom:legacy 165 function SYSTEM_CONFIG() external view returns (SystemConfig) { 166 return systemConfig; 167 } 168 169 /// @notice Getter function for the address of the guardian. 170 /// Public getter is legacy and will be removed in the future. Use `SuperchainConfig.guardian()` instead. 171 /// @return Address of the guardian. 172 /// @custom:legacy 173 function GUARDIAN() external view returns (address) { 174 return guardian(); 175 } 176 177 /// @notice Getter function for the address of the guardian. 178 /// Public getter is legacy and will be removed in the future. Use `SuperchainConfig.guardian()` instead. 179 /// @return Address of the guardian. 180 /// @custom:legacy 181 function guardian() public view returns (address) { 182 return superchainConfig.guardian(); 183 } 184 185 /// @notice Getter for the current paused status. 186 function paused() public view returns (bool) { 187 return superchainConfig.paused(); 188 } 189 190 /// @notice Getter for the proof maturity delay. 191 function proofMaturityDelaySeconds() public view returns (uint256) { 192 return PROOF_MATURITY_DELAY_SECONDS; 193 } 194 195 /// @notice Getter for the dispute game finality delay. 196 function disputeGameFinalityDelaySeconds() public view returns (uint256) { 197 return DISPUTE_GAME_FINALITY_DELAY_SECONDS; 198 } 199 200 /// @notice Computes the minimum gas limit for a deposit. 201 /// The minimum gas limit linearly increases based on the size of the calldata. 202 /// This is to prevent users from creating L2 resource usage without paying for it. 203 /// This function can be used when interacting with the portal to ensure forwards 204 /// compatibility. 205 /// @param _byteCount Number of bytes in the calldata. 206 /// @return The minimum gas limit for a deposit. 207 function minimumGasLimit(uint64 _byteCount) public pure returns (uint64) { 208 return _byteCount * 16 + 21000; 209 } 210 211 /// @notice Accepts value so that users can send ETH directly to this contract and have the 212 /// funds be deposited to their address on L2. This is intended as a convenience 213 /// function for EOAs. Contracts should call the depositTransaction() function directly 214 /// otherwise any deposited funds will be lost due to address aliasing. 215 receive() external payable { 216 depositTransaction(msg.sender, msg.value, RECEIVE_DEFAULT_GAS_LIMIT, false, bytes("")); 217 } 218 219 /// @notice Accepts ETH value without triggering a deposit to L2. 220 /// This function mainly exists for the sake of the migration between the legacy 221 /// Optimism system and Bedrock. 222 function donateETH() external payable { 223 // Intentionally empty. 224 } 225 226 /// @notice Getter for the resource config. 227 /// Used internally by the ResourceMetering contract. 228 /// The SystemConfig is the source of truth for the resource config. 229 /// @return ResourceMetering ResourceConfig 230 function _resourceConfig() internal view override returns (ResourceMetering.ResourceConfig memory) { 231 return systemConfig.resourceConfig(); 232 } 233 234 /// @notice Proves a withdrawal transaction. 235 /// @param _tx Withdrawal transaction to finalize. 236 /// @param _disputeGameIndex Index of the dispute game to prove the withdrawal against. 237 /// @param _outputRootProof Inclusion proof of the L2ToL1MessagePasser contract's storage root. 238 /// @param _withdrawalProof Inclusion proof of the withdrawal in L2ToL1MessagePasser contract. 239 function proveWithdrawalTransaction( 240 Types.WithdrawalTransaction memory _tx, 241 uint256 _disputeGameIndex, 242 Types.OutputRootProof calldata _outputRootProof, 243 bytes[] calldata _withdrawalProof 244 ) 245 external 246 whenNotPaused 247 { 248 // Prevent users from creating a deposit transaction where this address is the message 249 // sender on L2. Because this is checked here, we do not need to check again in 250 // `finalizeWithdrawalTransaction`. 251 require(_tx.target != address(this), "OptimismPortal: you cannot send messages to the portal contract"); 252 253 // Fetch the dispute game proxy from the `DisputeGameFactory` contract. 254 (GameType gameType,, IDisputeGame gameProxy) = disputeGameFactory.gameAtIndex(_disputeGameIndex); 255 Claim outputRoot = gameProxy.rootClaim(); 256 257 // The game type of the dispute game must be the respected game type. 258 require(gameType.raw() == respectedGameType.raw(), "OptimismPortal: invalid game type"); 259 260 // Verify that the output root can be generated with the elements in the proof. 261 require( 262 outputRoot.raw() == Hashing.hashOutputRootProof(_outputRootProof), 263 "OptimismPortal: invalid output root proof" 264 ); 265 266 // Load the ProvenWithdrawal into memory, using the withdrawal hash as a unique identifier. 267 bytes32 withdrawalHash = Hashing.hashWithdrawal(_tx); 268 ProvenWithdrawal memory provenWithdrawal = provenWithdrawals[withdrawalHash]; 269 270 // We generally want to prevent users from proving the same withdrawal multiple times 271 // because each successive proof will update the timestamp. A malicious user can take 272 // advantage of this to prevent other users from finalizing their withdrawal. However, 273 // in the case that an honest user proves their withdrawal against a dispute game that 274 // resolves against the root claim, or the dispute game is blacklisted, we allow 275 // re-proving the withdrawal against a new proposal. 276 IDisputeGame oldGame = provenWithdrawal.disputeGameProxy; 277 require( 278 provenWithdrawal.timestamp == 0 || oldGame.status() == GameStatus.CHALLENGER_WINS 279 || disputeGameBlacklist[oldGame] || oldGame.gameType().raw() != respectedGameType.raw(), 280 "OptimismPortal: withdrawal hash has already been proven, and the old dispute game is not invalid" 281 ); 282 283 // Compute the storage slot of the withdrawal hash in the L2ToL1MessagePasser contract. 284 // Refer to the Solidity documentation for more information on how storage layouts are 285 // computed for mappings. 286 bytes32 storageKey = keccak256( 287 abi.encode( 288 withdrawalHash, 289 uint256(0) // The withdrawals mapping is at the first slot in the layout. 290 ) 291 ); 292 293 // Verify that the hash of this withdrawal was stored in the L2toL1MessagePasser contract 294 // on L2. If this is true, under the assumption that the SecureMerkleTrie does not have 295 // bugs, then we know that this withdrawal was actually triggered on L2 and can therefore 296 // be relayed on L1. 297 require( 298 SecureMerkleTrie.verifyInclusionProof( 299 abi.encode(storageKey), hex"01", _withdrawalProof, _outputRootProof.messagePasserStorageRoot 300 ), 301 "OptimismPortal: invalid withdrawal inclusion proof" 302 ); 303 304 // Designate the withdrawalHash as proven by storing the `disputeGameProxy` & `timestamp` in the 305 // `provenWithdrawals` mapping. A `withdrawalHash` can only be proven once unless the dispute game it proved 306 // against resolves against the favor of the root claim. 307 provenWithdrawals[withdrawalHash] = 308 ProvenWithdrawal({ disputeGameProxy: gameProxy, timestamp: uint64(block.timestamp) }); 309 310 // Emit a `WithdrawalProven` event. 311 emit WithdrawalProven(withdrawalHash, _tx.sender, _tx.target); 312 } 313 314 /// @notice Finalizes a withdrawal transaction. 315 /// @param _tx Withdrawal transaction to finalize. 316 function finalizeWithdrawalTransaction(Types.WithdrawalTransaction memory _tx) external whenNotPaused { 317 // Make sure that the l2Sender has not yet been set. The l2Sender is set to a value other 318 // than the default value when a withdrawal transaction is being finalized. This check is 319 // a defacto reentrancy guard. 320 require( 321 l2Sender == Constants.DEFAULT_L2_SENDER, "OptimismPortal: can only trigger one withdrawal per transaction" 322 ); 323 324 // Compute the withdrawal hash. 325 bytes32 withdrawalHash = Hashing.hashWithdrawal(_tx); 326 327 // Check that the withdrawal can be finalized. 328 checkWithdrawal(withdrawalHash); 329 330 // Mark the withdrawal as finalized so it can't be replayed. 331 finalizedWithdrawals[withdrawalHash] = true; 332 333 // Set the l2Sender so contracts know who triggered this withdrawal on L2. 334 l2Sender = _tx.sender; 335 336 // Trigger the call to the target contract. We use a custom low level method 337 // SafeCall.callWithMinGas to ensure two key properties 338 // 1. Target contracts cannot force this call to run out of gas by returning a very large 339 // amount of data (and this is OK because we don't care about the returndata here). 340 // 2. The amount of gas provided to the execution context of the target is at least the 341 // gas limit specified by the user. If there is not enough gas in the current context 342 // to accomplish this, `callWithMinGas` will revert. 343 bool success = SafeCall.callWithMinGas(_tx.target, _tx.gasLimit, _tx.value, _tx.data); 344 345 // Reset the l2Sender back to the default value. 346 l2Sender = Constants.DEFAULT_L2_SENDER; 347 348 // All withdrawals are immediately finalized. Replayability can 349 // be achieved through contracts built on top of this contract 350 emit WithdrawalFinalized(withdrawalHash, success); 351 352 // Reverting here is useful for determining the exact gas cost to successfully execute the 353 // sub call to the target contract if the minimum gas limit specified by the user would not 354 // be sufficient to execute the sub call. 355 if (!success && tx.origin == Constants.ESTIMATION_ADDRESS) { 356 revert("OptimismPortal: withdrawal failed"); 357 } 358 } 359 360 /// @notice Accepts deposits of ETH and data, and emits a TransactionDeposited event for use in 361 /// deriving deposit transactions. Note that if a deposit is made by a contract, its 362 /// address will be aliased when retrieved using `tx.origin` or `msg.sender`. Consider 363 /// using the CrossDomainMessenger contracts for a simpler developer experience. 364 /// @param _to Target address on L2. 365 /// @param _value ETH value to send to the recipient. 366 /// @param _gasLimit Amount of L2 gas to purchase by burning gas on L1. 367 /// @param _isCreation Whether or not the transaction is a contract creation. 368 /// @param _data Data to trigger the recipient with. 369 function depositTransaction( 370 address _to, 371 uint256 _value, 372 uint64 _gasLimit, 373 bool _isCreation, 374 bytes memory _data 375 ) 376 public 377 payable 378 metered(_gasLimit) 379 { 380 // Just to be safe, make sure that people specify address(0) as the target when doing 381 // contract creations. 382 if (_isCreation) { 383 require(_to == address(0), "OptimismPortal: must send to address(0) when creating a contract"); 384 } 385 386 // Prevent depositing transactions that have too small of a gas limit. Users should pay 387 // more for more resource usage. 388 require(_gasLimit >= minimumGasLimit(uint64(_data.length)), "OptimismPortal: gas limit too small"); 389 390 // Prevent the creation of deposit transactions that have too much calldata. This gives an 391 // upper limit on the size of unsafe blocks over the p2p network. 120kb is chosen to ensure 392 // that the transaction can fit into the p2p network policy of 128kb even though deposit 393 // transactions are not gossipped over the p2p network. 394 require(_data.length <= 120_000, "OptimismPortal: data too large"); 395 396 // Transform the from-address to its alias if the caller is a contract. 397 address from = msg.sender; 398 if (msg.sender != tx.origin) { 399 from = AddressAliasHelper.applyL1ToL2Alias(msg.sender); 400 } 401 402 // Compute the opaque data that will be emitted as part of the TransactionDeposited event. 403 // We use opaque data so that we can update the TransactionDeposited event in the future 404 // without breaking the current interface. 405 bytes memory opaqueData = abi.encodePacked(msg.value, _value, _gasLimit, _isCreation, _data); 406 407 // Emit a TransactionDeposited event so that the rollup node can derive a deposit 408 // transaction for this deposit. 409 emit TransactionDeposited(from, _to, DEPOSIT_VERSION, opaqueData); 410 } 411 412 /// @notice Blacklists a dispute game. Should only be used in the event that a dispute game resolves incorrectly. 413 /// @param _disputeGame Dispute game to blacklist. 414 function blacklistDisputeGame(IDisputeGame _disputeGame) external { 415 require(msg.sender == guardian(), "OptimismPortal: only the guardian can blacklist dispute games"); 416 disputeGameBlacklist[_disputeGame] = true; 417 } 418 419 /// @notice Sets the respected game type. Changing this value can alter the security properties of the system, 420 /// depending on the new game's behavior. 421 /// @param _gameType The game type to consult for output proposals. 422 function setRespectedGameType(GameType _gameType) external { 423 require(msg.sender == guardian(), "OptimismPortal: only the guardian can set the respected game type"); 424 respectedGameType = _gameType; 425 respectedGameTypeUpdatedAt = uint64(block.timestamp); 426 } 427 428 /// @notice Checks if a withdrawal can be finalized. This function will revert if the withdrawal cannot be 429 /// finalized, and otherwise has no side-effects. 430 /// @param _withdrawalHash Hash of the withdrawal to check. 431 function checkWithdrawal(bytes32 _withdrawalHash) public view { 432 ProvenWithdrawal memory provenWithdrawal = provenWithdrawals[_withdrawalHash]; 433 IDisputeGame disputeGameProxy = provenWithdrawal.disputeGameProxy; 434 435 // The dispute game must not be blacklisted. 436 require(!disputeGameBlacklist[disputeGameProxy], "OptimismPortal: dispute game has been blacklisted"); 437 438 // A withdrawal can only be finalized if it has been proven. We know that a withdrawal has 439 // been proven at least once when its timestamp is non-zero. Unproven withdrawals will have 440 // a timestamp of zero. 441 require(provenWithdrawal.timestamp != 0, "OptimismPortal: withdrawal has not been proven yet"); 442 443 uint64 createdAt = disputeGameProxy.createdAt().raw(); 444 445 // As a sanity check, we make sure that the proven withdrawal's timestamp is greater than 446 // starting timestamp inside the Dispute Game. Not strictly necessary but extra layer of 447 // safety against weird bugs in the proving step. 448 require( 449 provenWithdrawal.timestamp > createdAt, 450 "OptimismPortal: withdrawal timestamp less than dispute game creation timestamp" 451 ); 452 453 // A proven withdrawal must wait at least `PROOF_MATURITY_DELAY_SECONDS` before finalizing. 454 require( 455 block.timestamp - provenWithdrawal.timestamp > PROOF_MATURITY_DELAY_SECONDS, 456 "OptimismPortal: proven withdrawal has not matured yet" 457 ); 458 459 // A proven withdrawal must wait until the dispute game it was proven against has been 460 // resolved in favor of the root claim (the output proposal). This is to prevent users 461 // from finalizing withdrawals proven against non-finalized output roots. 462 require( 463 disputeGameProxy.status() == GameStatus.DEFENDER_WINS, 464 "OptimismPortal: output proposal has not been finalized yet" 465 ); 466 467 // The game type of the dispute game must be the respected game type. This was also checked in 468 // `proveWithdrawalTransaction`, but we check it again in case the respected game type has changed since 469 // the withdrawal was proven. 470 require(disputeGameProxy.gameType().raw() == respectedGameType.raw(), "OptimismPortal: invalid game type"); 471 472 // The game must have been created after `respectedGameTypeUpdatedAt`. This is to prevent users from creating 473 // invalid disputes against a deployed game type while the off-chain challenge agents are not watching. 474 require( 475 createdAt >= respectedGameTypeUpdatedAt, 476 "OptimismPortal: dispute game created before respected game type was updated" 477 ); 478 479 // Before a withdrawal can be finalized, the dispute game it was proven against must have been 480 // resolved for at least `DISPUTE_GAME_FINALITY_DELAY_SECONDS`. This is to allow for manual 481 // intervention in the event that a dispute game is resolved incorrectly. 482 require( 483 block.timestamp - disputeGameProxy.resolvedAt().raw() > DISPUTE_GAME_FINALITY_DELAY_SECONDS, 484 "OptimismPortal: output proposal in air-gap" 485 ); 486 487 // Check that this withdrawal has not already been finalized, this is replay protection. 488 require(!finalizedWithdrawals[_withdrawalHash], "OptimismPortal: withdrawal has already been finalized"); 489 } 490 }