Commit ed817c7c authored by Mark Tyneway's avatar Mark Tyneway Committed by GitHub

contracts-bedrock: port custom gas token to portal2 (#10780)

* contracts-bedrock: port custom gas token to portal2

Ports the custom gas token feature to `OptimismPortal2`.
This will enable fault proofs to run on custom gas token
chains.

* lint: fix

* tests: update

* tests: update
parent 67de0aff
......@@ -40,8 +40,8 @@
"sourceCodeHash": "0x9fe0a9001edecd2a04daada4ca9e17d66141b1c982f73653493b4703d2c675c4"
},
"src/L1/OptimismPortal2.sol": {
"initCodeHash": "0x45cae622788a795c2fc4f4bc8e6b85d8edf284a1dc20e1b5fa01e88d737deb23",
"sourceCodeHash": "0xea564dbff9831ad1bf0c1b345fbc3da4675cf112d2605ba94e1ef5c7b745b7ae"
"initCodeHash": "0x7f5122fb6d5c1123c870f936f8a33f752190ae90932434ad9b726fb2b582746f",
"sourceCodeHash": "0x9af4d957b9d554b9926ad27572766d6f22c515f1ed6f620389b32c46ebce4c7b"
},
"src/L1/OptimismPortalInterop.sol": {
"initCodeHash": "0x4ab4c99bd776d1817f7475161db0ce47e735a91bb9fb486338238aa762fe0909",
......
......@@ -19,6 +19,19 @@
"stateMutability": "payable",
"type": "receive"
},
{
"inputs": [],
"name": "balance",
"outputs": [
{
"internalType": "uint256",
"name": "",
"type": "uint256"
}
],
"stateMutability": "view",
"type": "function"
},
{
"inputs": [
{
......@@ -50,6 +63,44 @@
"stateMutability": "view",
"type": "function"
},
{
"inputs": [
{
"internalType": "address",
"name": "_to",
"type": "address"
},
{
"internalType": "uint256",
"name": "_mint",
"type": "uint256"
},
{
"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": "depositERC20Transaction",
"outputs": [],
"stateMutability": "nonpayable",
"type": "function"
},
{
"inputs": [
{
......@@ -551,6 +602,34 @@
"stateMutability": "view",
"type": "function"
},
{
"inputs": [
{
"internalType": "address",
"name": "_token",
"type": "address"
},
{
"internalType": "uint8",
"name": "_decimals",
"type": "uint8"
},
{
"internalType": "bytes32",
"name": "_name",
"type": "bytes32"
},
{
"internalType": "bytes32",
"name": "_symbol",
"type": "bytes32"
}
],
"name": "setGasPayingToken",
"outputs": [],
"stateMutability": "nonpayable",
"type": "function"
},
{
"inputs": [
{
......@@ -600,7 +679,7 @@
"type": "string"
}
],
"stateMutability": "view",
"stateMutability": "pure",
"type": "function"
},
{
......@@ -782,6 +861,21 @@
"name": "LargeCalldata",
"type": "error"
},
{
"inputs": [],
"name": "NoValue",
"type": "error"
},
{
"inputs": [],
"name": "NonReentrant",
"type": "error"
},
{
"inputs": [],
"name": "OnlyCustomGasToken",
"type": "error"
},
{
"inputs": [],
"name": "OutOfGas",
......@@ -792,6 +886,11 @@
"name": "SmallGasLimit",
"type": "error"
},
{
"inputs": [],
"name": "TransferFailed",
"type": "error"
},
{
"inputs": [],
"name": "Unauthorized",
......
This source diff could not be displayed because it is too large. You can view the blob instead.
......@@ -120,9 +120,9 @@
},
{
"bytes": "32",
"label": "spacer_61_0_32",
"label": "_balance",
"offset": 0,
"slot": "61",
"type": "bytes32"
"type": "uint256"
}
]
\ No newline at end of file
......@@ -14,6 +14,10 @@ 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 { SafeERC20 } from "@openzeppelin/contracts/token/ERC20/utils/SafeERC20.sol";
import { IERC20 } from "@openzeppelin/contracts/token/ERC20/IERC20.sol";
import { L1Block } from "src/L2/L1Block.sol";
import { Predeploys } from "src/libraries/Predeploys.sol";
import "src/libraries/PortalErrors.sol";
import "src/dispute/lib/Types.sol";
......@@ -24,6 +28,9 @@ import "src/dispute/lib/Types.sol";
/// 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 Allows for interactions with non standard ERC20 tokens.
using SafeERC20 for IERC20;
/// @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.
......@@ -45,6 +52,9 @@ contract OptimismPortal2 is Initializable, ResourceMetering, ISemver {
/// @notice The L2 gas limit set when eth is deposited using the receive() function.
uint64 internal constant RECEIVE_DEFAULT_GAS_LIMIT = 100_000;
/// @notice The L2 gas limit for system deposit transactions that are initiated from L1.
uint32 internal constant SYSTEM_DEPOSIT_GAS_LIMIT = 200_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.
......@@ -94,9 +104,12 @@ contract OptimismPortal2 is Initializable, ResourceMetering, ISemver {
/// @notice Mapping of withdrawal hashes to addresses that have submitted a proof for the withdrawal.
mapping(bytes32 => address[]) public proofSubmitters;
/// @custom:spacer _balance (custom gas token)
/// @notice Spacer for forwards compatibility.
bytes32 private spacer_61_0_32;
/// @notice Represents the amount of native asset minted in L2. This may not
/// be 100% accurate due to the ability to send ether to the contract
/// without triggering a deposit transaction. It also is used to prevent
/// overflows for L2 account balances when custom gas tokens are used.
/// It is not safe to trust `ERC20.balanceOf` as it may lie.
uint256 internal _balance;
/// @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
......@@ -140,8 +153,10 @@ contract OptimismPortal2 is Initializable, ResourceMetering, ISemver {
}
/// @notice Semantic version.
/// @custom:semver 3.10.0
string public constant version = "3.10.0";
/// @custom:semver 3.11.0-beta.1
function version() public pure virtual returns (string memory) {
return "3.11.0-beta.1";
}
/// @notice Constructs the OptimismPortal contract.
constructor(uint256 _proofMaturityDelaySeconds, uint256 _disputeGameFinalityDelaySeconds) {
......@@ -189,6 +204,16 @@ contract OptimismPortal2 is Initializable, ResourceMetering, ISemver {
__ResourceMetering_init();
}
/// @notice Getter for the balance of the contract.
function balance() public view returns (uint256) {
(address token,) = gasPayingToken();
if (token == Constants.ETHER) {
return address(this).balance;
} else {
return _balance;
}
}
/// @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.
......@@ -238,6 +263,11 @@ contract OptimismPortal2 is Initializable, ResourceMetering, ISemver {
// Intentionally empty.
}
/// @notice Returns the gas paying token and its decimals.
function gasPayingToken() internal view returns (address addr_, uint8 decimals_) {
(addr_, decimals_) = systemConfig.gasPayingToken();
}
/// @notice Getter for the resource config.
/// Used internally by the ResourceMetering contract.
/// The SystemConfig is the source of truth for the resource config.
......@@ -263,7 +293,7 @@ contract OptimismPortal2 is Initializable, ResourceMetering, ISemver {
// 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");
if (_tx.target == address(this)) revert BadTarget();
// Fetch the dispute game proxy from the `DisputeGameFactory` contract.
(GameType gameType,, IDisputeGame gameProxy) = disputeGameFactory.gameAtIndex(_disputeGameIndex);
......@@ -346,9 +376,7 @@ contract OptimismPortal2 is Initializable, ResourceMetering, ISemver {
// 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"
);
if (l2Sender != Constants.DEFAULT_L2_SENDER) revert NonReentrant();
// Compute the withdrawal hash.
bytes32 withdrawalHash = Hashing.hashWithdrawal(_tx);
......@@ -362,14 +390,50 @@ contract OptimismPortal2 is Initializable, ResourceMetering, ISemver {
// 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);
bool success;
(address token,) = gasPayingToken();
if (token == Constants.ETHER) {
// 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.
success = SafeCall.callWithMinGas(_tx.target, _tx.gasLimit, _tx.value, _tx.data);
} else {
// Cannot call the token contract directly from the portal. This would allow an attacker
// to call approve from a withdrawal and drain the balance of the portal.
if (_tx.target == token) revert BadTarget();
// Only transfer value when a non zero value is specified. This saves gas in the case of
// using the standard bridge or arbitrary message passing.
if (_tx.value != 0) {
// Update the contracts internal accounting of the amount of native asset in L2.
_balance -= _tx.value;
// Read the balance of the target contract before the transfer so the consistency
// of the transfer can be checked afterwards.
uint256 startBalance = IERC20(token).balanceOf(address(this));
// Transfer the ERC20 balance to the target, accounting for non standard ERC20
// implementations that may not return a boolean. This reverts if the low level
// call is not successful.
IERC20(token).safeTransfer({ to: _tx.target, value: _tx.value });
// The balance must be transferred exactly.
if (IERC20(token).balanceOf(address(this)) != startBalance - _tx.value) {
revert TransferFailed();
}
}
// Make a call to the target contract only if there is calldata.
if (_tx.data.length != 0) {
success = SafeCall.callWithMinGas(_tx.target, _tx.gasLimit, 0, _tx.data);
} else {
success = true;
}
}
// Reset the l2Sender back to the default value.
l2Sender = Constants.DEFAULT_L2_SENDER;
......@@ -386,6 +450,55 @@ contract OptimismPortal2 is Initializable, ResourceMetering, ISemver {
}
}
/// @notice Entrypoint to depositing an ERC20 token as a custom gas token.
/// This function depends on a well formed ERC20 token. There are only
/// so many checks that can be done on chain for this so it is assumed
/// that chain operators will deploy chains with well formed ERC20 tokens.
/// @param _to Target address on L2.
/// @param _mint Units of ERC20 token to deposit into L2.
/// @param _value Units of ERC20 token to send on L2 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 depositERC20Transaction(
address _to,
uint256 _mint,
uint256 _value,
uint64 _gasLimit,
bool _isCreation,
bytes memory _data
)
public
metered(_gasLimit)
{
// Can only be called if an ERC20 token is used for gas paying on L2
(address token,) = gasPayingToken();
if (token == Constants.ETHER) revert OnlyCustomGasToken();
// Gives overflow protection for L2 account balances.
_balance += _mint;
// Get the balance of the portal before the transfer.
uint256 startBalance = IERC20(token).balanceOf(address(this));
// Take ownership of the token. It is assumed that the user has given the portal an approval.
IERC20(token).safeTransferFrom({ from: msg.sender, to: address(this), value: _mint });
// Double check that the portal now has the exact amount of token.
if (IERC20(token).balanceOf(address(this)) != startBalance + _mint) {
revert TransferFailed();
}
_depositTransaction({
_to: _to,
_mint: _mint,
_value: _value,
_gasLimit: _gasLimit,
_isCreation: _isCreation,
_data: _data
});
}
/// @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
......@@ -405,6 +518,36 @@ contract OptimismPortal2 is Initializable, ResourceMetering, ISemver {
public
payable
metered(_gasLimit)
{
(address token,) = gasPayingToken();
if (token != Constants.ETHER && msg.value != 0) revert NoValue();
_depositTransaction({
_to: _to,
_mint: msg.value,
_value: _value,
_gasLimit: _gasLimit,
_isCreation: _isCreation,
_data: _data
});
}
/// @notice Common logic for creating deposit transactions.
/// @param _to Target address on L2.
/// @param _mint Units of asset to deposit into L2.
/// @param _value Units of asset to send on L2 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 _mint,
uint256 _value,
uint64 _gasLimit,
bool _isCreation,
bytes memory _data
)
internal
{
// Just to be safe, make sure that people specify address(0) as the target when doing
// contract creations.
......@@ -429,13 +572,38 @@ contract OptimismPortal2 is Initializable, ResourceMetering, ISemver {
// 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);
bytes memory opaqueData = abi.encodePacked(_mint, _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 Sets the gas paying token for the L2 system. This token is used as the
/// L2 native asset. Only the SystemConfig contract can call this function.
function setGasPayingToken(address _token, uint8 _decimals, bytes32 _name, bytes32 _symbol) external {
if (msg.sender != address(systemConfig)) revert Unauthorized();
// Set L2 deposit gas as used without paying burning gas. Ensures that deposits cannot use too much L2 gas.
// This value must be large enough to cover the cost of calling `L1Block.setGasPayingToken`.
useGas(SYSTEM_DEPOSIT_GAS_LIMIT);
// Emit the special deposit transaction directly that sets the gas paying
// token in the L1Block predeploy contract.
emit TransactionDeposited(
Constants.DEPOSITOR_ACCOUNT,
Predeploys.L1_BLOCK_ATTRIBUTES,
DEPOSIT_VERSION,
abi.encodePacked(
uint256(0), // mint
uint256(0), // value
uint64(SYSTEM_DEPOSIT_GAS_LIMIT), // gasLimit
false, // isCreation,
abi.encodeCall(L1Block.setGasPayingToken, (_token, _decimals, _name, _symbol))
)
);
}
/// @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 {
......
......@@ -3,6 +3,7 @@ pragma solidity 0.8.15;
// Testing utilities
import { stdError } from "forge-std/Test.sol";
import { VmSafe } from "forge-std/Vm.sol";
import { CommonTest } from "test/setup/CommonTest.sol";
import { NextImpl } from "test/mocks/NextImpl.sol";
......@@ -12,14 +13,18 @@ import { EIP1967Helper } from "test/mocks/EIP1967Helper.sol";
import { Types } from "src/libraries/Types.sol";
import { Hashing } from "src/libraries/Hashing.sol";
import { Constants } from "src/libraries/Constants.sol";
import { Predeploys } from "src/libraries/Predeploys.sol";
// Target contract dependencies
import { Proxy } from "src/universal/Proxy.sol";
import { ResourceMetering } from "src/L1/ResourceMetering.sol";
import { AddressAliasHelper } from "src/vendor/AddressAliasHelper.sol";
import { SystemConfig } from "src/L1/SystemConfig.sol";
import { L1Block } from "src/L2/L1Block.sol";
import { SuperchainConfig } from "src/L1/SuperchainConfig.sol";
import { OptimismPortal2 } from "src/L1/OptimismPortal2.sol";
import { GasPayingToken } from "src/libraries/GasPayingToken.sol";
import { MockERC20 } from "solmate/test/utils/mocks/MockERC20.sol";
import { FaultDisputeGame, IDisputeGame } from "src/dispute/FaultDisputeGame.sol";
import "src/dispute/lib/Types.sol";
......@@ -32,6 +37,12 @@ contract OptimismPortal2_Test is CommonTest {
super.enableFaultProofs();
super.setUp();
// zero out contracts that should not be used
assembly {
sstore(l2OutputOracle.slot, 0)
sstore(optimismPortal.slot, 0)
}
depositor = makeAddr("depositor");
}
......@@ -286,6 +297,127 @@ contract OptimismPortal2_Test is CommonTest {
});
assertEq(address(optimismPortal2).balance, _mint);
}
/// @dev Tests that the gas paying token can be set.
function testFuzz_setGasPayingToken_succeeds(
address _token,
uint8 _decimals,
bytes32 _name,
bytes32 _symbol
)
external
{
vm.expectEmit(address(optimismPortal2));
emit TransactionDeposited(
0xDeaDDEaDDeAdDeAdDEAdDEaddeAddEAdDEAd0001,
Predeploys.L1_BLOCK_ATTRIBUTES,
0,
abi.encodePacked(
uint256(0), // mint
uint256(0), // value
uint64(200_000), // gasLimit
false, // isCreation,
abi.encodeCall(L1Block.setGasPayingToken, (_token, _decimals, _name, _symbol))
)
);
vm.prank(address(systemConfig));
optimismPortal2.setGasPayingToken({ _token: _token, _decimals: _decimals, _name: _name, _symbol: _symbol });
}
/// @notice Ensures that the deposit event is correct for the `setGasPayingToken`
/// code path that manually emits a deposit transaction outside of the
/// `depositTransaction` function. This is a simple differential test.
function test_setGasPayingToken_correctEvent_succeeds(
address _token,
string memory _name,
string memory _symbol
)
external
{
vm.assume(bytes(_name).length <= 32);
vm.assume(bytes(_symbol).length <= 32);
bytes32 name = GasPayingToken.sanitize(_name);
bytes32 symbol = GasPayingToken.sanitize(_symbol);
vm.recordLogs();
vm.prank(address(systemConfig));
optimismPortal2.setGasPayingToken({ _token: _token, _decimals: 18, _name: name, _symbol: symbol });
vm.prank(Constants.DEPOSITOR_ACCOUNT, Constants.DEPOSITOR_ACCOUNT);
optimismPortal2.depositTransaction({
_to: Predeploys.L1_BLOCK_ATTRIBUTES,
_value: 0,
_gasLimit: 200_000,
_isCreation: false,
_data: abi.encodeCall(L1Block.setGasPayingToken, (_token, 18, name, symbol))
});
VmSafe.Log[] memory logs = vm.getRecordedLogs();
assertEq(logs.length, 2);
VmSafe.Log memory systemPath = logs[0];
VmSafe.Log memory userPath = logs[1];
assertEq(systemPath.topics.length, 4);
assertEq(systemPath.topics.length, userPath.topics.length);
assertEq(systemPath.topics[0], userPath.topics[0]);
assertEq(systemPath.topics[1], userPath.topics[1]);
assertEq(systemPath.topics[2], userPath.topics[2]);
assertEq(systemPath.topics[3], userPath.topics[3]);
assertEq(systemPath.data, userPath.data);
}
/// @dev Tests that the gas paying token cannot be set by a non-system config.
function test_setGasPayingToken_notSystemConfig_fails(address _caller) external {
vm.assume(_caller != address(systemConfig));
vm.prank(_caller);
vm.expectRevert(Unauthorized.selector);
optimismPortal2.setGasPayingToken({ _token: address(0), _decimals: 0, _name: "", _symbol: "" });
}
/// @dev Tests that `depositERC20Transaction` reverts when the gas paying token is ether.
function test_depositERC20Transaction_noCustomGasToken_reverts() external {
// Check that the gas paying token is set to ether
(address token,) = systemConfig.gasPayingToken();
assertEq(token, Constants.ETHER);
vm.expectRevert(OnlyCustomGasToken.selector);
optimismPortal2.depositERC20Transaction(address(0), 0, 0, 0, false, "");
}
function test_depositERC20Transaction_balanceOverflow_reverts() external {
vm.mockCall(address(systemConfig), abi.encodeWithSignature("gasPayingToken()"), abi.encode(address(42), 18));
// The balance slot
vm.store(address(optimismPortal2), bytes32(uint256(61)), bytes32(type(uint256).max));
assertEq(optimismPortal2.balance(), type(uint256).max);
vm.expectRevert(stdError.arithmeticError);
optimismPortal2.depositERC20Transaction({
_to: address(0),
_mint: 1,
_value: 1,
_gasLimit: 10_000,
_isCreation: false,
_data: ""
});
}
/// @dev Tests that `balance()` returns the correct balance when the gas paying token is ether.
function testFuzz_balance_ether_succeeds(uint256 _amount) external {
// Check that the gas paying token is set to ether
(address token,) = systemConfig.gasPayingToken();
assertEq(token, Constants.ETHER);
// Increase the balance of the gas paying token
vm.deal(address(optimismPortal2), _amount);
// Check that the balance has been correctly updated
assertEq(optimismPortal2.balance(), address(optimismPortal2).balance);
}
}
contract OptimismPortal2_FinalizeWithdrawal_Test is CommonTest {
......@@ -313,8 +445,8 @@ contract OptimismPortal2_FinalizeWithdrawal_Test is CommonTest {
target: bob,
value: 100,
gasLimit: 100_000,
data: hex""
});
data: hex"aa" // includes calldata for ERC20 withdrawal test
});
// Get withdrawal proof data we can use for testing.
(_stateRoot, _storageRoot, _outputRoot, _withdrawalHash, _withdrawalProof) =
ffi.getProveWithdrawalTransactionInputs(_defaultTx);
......@@ -329,7 +461,7 @@ contract OptimismPortal2_FinalizeWithdrawal_Test is CommonTest {
}
/// @dev Setup the system for a ready-to-use state.
function setUp() public override {
function setUp() public virtual override {
_proposedBlockNumber = 0xFF;
game = FaultDisputeGame(
payable(
......@@ -351,7 +483,7 @@ contract OptimismPortal2_FinalizeWithdrawal_Test is CommonTest {
/// @dev Asserts that the reentrant call will revert.
function callPortalAndExpectRevert() external payable {
vm.expectRevert("OptimismPortal: can only trigger one withdrawal per transaction");
vm.expectRevert(NonReentrant.selector);
// Arguments here don't matter, as the require check is the first thing that happens.
// We assume that this has already been proven.
optimismPortal2.finalizeWithdrawalTransaction(_defaultTx);
......@@ -414,7 +546,7 @@ contract OptimismPortal2_FinalizeWithdrawal_Test is CommonTest {
/// @dev Tests that `proveWithdrawalTransaction` reverts when the target is the portal contract.
function test_proveWithdrawalTransaction_onSelfCall_reverts() external {
_defaultTx.target = address(optimismPortal2);
vm.expectRevert("OptimismPortal: you cannot send messages to the portal contract");
vm.expectRevert(BadTarget.selector);
optimismPortal2.proveWithdrawalTransaction({
_tx: _defaultTx,
_disputeGameIndex: _proposedGameIndex,
......@@ -452,9 +584,9 @@ contract OptimismPortal2_FinalizeWithdrawal_Test is CommonTest {
/// @dev Tests that `proveWithdrawalTransaction` reverts when the withdrawal has already been proven, and the new
/// game has the `CHALLENGER_WINS` status.
function test_proveWithdrawalTransaction_replayProve_differentGameChallengerWins_reverts() external {
vm.expectEmit(true, true, true, true);
vm.expectEmit(address(optimismPortal2));
emit WithdrawalProven(_withdrawalHash, alice, bob);
vm.expectEmit(true, true, true, true);
vm.expectEmit(address(optimismPortal2));
emit WithdrawalProvenExtension1(_withdrawalHash, address(this));
optimismPortal2.proveWithdrawalTransaction({
_tx: _defaultTx,
......@@ -671,12 +803,12 @@ contract OptimismPortal2_FinalizeWithdrawal_Test is CommonTest {
}
/// @dev Tests that `finalizeWithdrawalTransaction` succeeds.
function test_finalizeWithdrawalTransaction_provenWithdrawalHash_succeeds() external {
function test_finalizeWithdrawalTransaction_provenWithdrawalHash_ether_succeeds() external {
uint256 bobBalanceBefore = address(bob).balance;
vm.expectEmit(true, true, true, true);
vm.expectEmit(address(optimismPortal2));
emit WithdrawalProven(_withdrawalHash, alice, bob);
vm.expectEmit(true, true, true, true);
vm.expectEmit(address(optimismPortal2));
emit WithdrawalProvenExtension1(_withdrawalHash, address(this));
optimismPortal2.proveWithdrawalTransaction({
_tx: _defaultTx,
......@@ -758,6 +890,30 @@ contract OptimismPortal2_FinalizeWithdrawal_Test is CommonTest {
assert(address(bob).balance == bobBalanceBefore + 100);
}
/// @dev Tests that `finalizeWithdrawalTransaction` succeeds.
function test_finalizeWithdrawalTransaction_provenWithdrawalHash_nonEther_targetToken_reverts() external {
vm.mockCall(
address(systemConfig),
abi.encodeWithSignature("gasPayingToken()"),
abi.encode(address(_defaultTx.target), 18)
);
optimismPortal2.proveWithdrawalTransaction({
_tx: _defaultTx,
_disputeGameIndex: _proposedGameIndex,
_outputRootProof: _outputRootProof,
_withdrawalProof: _withdrawalProof
});
// Warp to after the finalization period
game.resolveClaim(0, 0);
game.resolve();
vm.warp(block.timestamp + optimismPortal2.proofMaturityDelaySeconds() + 1);
vm.expectRevert(BadTarget.selector);
optimismPortal2.finalizeWithdrawalTransaction(_defaultTx);
}
/// @dev Tests that `finalizeWithdrawalTransaction` reverts if the contract is paused.
function test_finalizeWithdrawalTransaction_paused_reverts() external {
vm.prank(optimismPortal2.guardian());
......@@ -782,9 +938,9 @@ contract OptimismPortal2_FinalizeWithdrawal_Test is CommonTest {
function test_finalizeWithdrawalTransaction_ifWithdrawalProofNotOldEnough_reverts() external {
uint256 bobBalanceBefore = address(bob).balance;
vm.expectEmit(true, true, true, true);
vm.expectEmit(address(optimismPortal2));
emit WithdrawalProven(_withdrawalHash, alice, bob);
vm.expectEmit(true, true, true, true);
vm.expectEmit(address(optimismPortal2));
emit WithdrawalProvenExtension1(_withdrawalHash, address(this));
optimismPortal2.proveWithdrawalTransaction({
_tx: _defaultTx,
......@@ -1169,9 +1325,9 @@ contract OptimismPortal2_FinalizeWithdrawal_Test is CommonTest {
/// @dev Tests that `finalizeWithdrawalTransaction` reverts if the respected game type was updated after the
/// dispute game was created.
function test_finalizeWithdrawalTransaction_gameOlderThanRespectedGameTypeUpdate_reverts() external {
vm.expectEmit(true, true, true, true);
vm.expectEmit(address(optimismPortal2));
emit WithdrawalProven(_withdrawalHash, alice, bob);
vm.expectEmit(true, true, true, true);
vm.expectEmit(address(optimismPortal2));
emit WithdrawalProvenExtension1(_withdrawalHash, address(this));
optimismPortal2.proveWithdrawalTransaction({
_tx: _defaultTx,
......@@ -1360,3 +1516,333 @@ contract OptimismPortal2_ResourceFuzz_Test is CommonTest {
});
}
}
contract OptimismPortal2WithMockERC20_Test is OptimismPortal2_FinalizeWithdrawal_Test {
MockERC20 token;
function setUp() public virtual override {
super.setUp();
token = new MockERC20("Test", "TST", 18);
}
function depositERC20Transaction(
address _from,
address _to,
uint256 _mint,
uint256 _value,
uint64 _gasLimit,
bool _isCreation,
bytes memory _data
)
internal
{
if (_isCreation) {
_to = address(0);
}
vm.assume(_data.length <= 120_000);
ResourceMetering.ResourceConfig memory rcfg = systemConfig.resourceConfig();
_gasLimit =
uint64(bound(_gasLimit, optimismPortal2.minimumGasLimit(uint64(_data.length)), rcfg.maxResourceLimit));
// Mint the token to the contract and approve the token for the portal
token.mint(address(this), _mint);
token.approve(address(optimismPortal2), _mint);
// Mock the gas paying token to be the ERC20 token
vm.mockCall(address(systemConfig), abi.encodeWithSignature("gasPayingToken()"), abi.encode(address(token), 18));
bytes memory opaqueData = abi.encodePacked(_mint, _value, _gasLimit, _isCreation, _data);
vm.expectEmit(address(optimismPortal2));
emit TransactionDeposited(
_from, // from
_to,
uint256(0), // DEPOSIT_VERSION
opaqueData
);
// Deposit the token into the portal
optimismPortal.depositERC20Transaction(_to, _mint, _value, _gasLimit, _isCreation, _data);
// Assert final balance equals the deposited amount
assertEq(token.balanceOf(address(optimismPortal2)), _mint);
assertEq(optimismPortal2.balance(), _mint);
}
/// @dev Tests that `depositERC20Transaction` succeeds when msg.sender == tx.origin.
function testFuzz_depositERC20Transaction_senderIsOrigin_succeeds(
address _to,
uint256 _mint,
uint256 _value,
uint64 _gasLimit,
bool _isCreation,
bytes memory _data
)
external
{
// Ensure that msg.sender == tx.origin
vm.startPrank(address(this), address(this));
depositERC20Transaction({
_from: address(this),
_to: _to,
_mint: _mint,
_value: _value,
_gasLimit: _gasLimit,
_isCreation: _isCreation,
_data: _data
});
}
/// @dev Tests that `depositERC20Transaction` succeeds when msg.sender != tx.origin.
function testFuzz_depositERC20Transaction_senderNotOrigin_succeeds(
address _to,
uint256 _mint,
uint256 _value,
uint64 _gasLimit,
bool _isCreation,
bytes memory _data
)
external
{
// Ensure that msg.sender != tx.origin
vm.startPrank(address(this), address(1));
depositERC20Transaction({
_from: AddressAliasHelper.applyL1ToL2Alias(address(this)),
_to: _to,
_mint: _mint,
_value: _value,
_gasLimit: _gasLimit,
_isCreation: _isCreation,
_data: _data
});
}
/// @dev Tests that `depositERC20Transaction` reverts when not enough of the token is approved.
function test_depositERC20Transaction_notEnoughAmount_reverts() external {
// Mock the gas paying token to be the ERC20 token
vm.mockCall(address(systemConfig), abi.encodeWithSignature("gasPayingToken()"), abi.encode(address(token), 18));
vm.expectRevert(stdError.arithmeticError);
// Deposit the token into the portal
optimismPortal2.depositERC20Transaction(address(0), 1, 0, 0, false, "");
}
/// @dev Tests that `depositERC20Transaction` reverts when token balance does not update correctly after transfer.
function test_depositERC20Transaction_incorrectTokenBalance_reverts() external {
// Mint the token to the contract and approve the token for the portal
token.mint(address(this), 100);
token.approve(address(optimismPortal2), 100);
// Mock the gas paying token to be the ERC20 token
vm.mockCall(address(systemConfig), abi.encodeWithSignature("gasPayingToken()"), abi.encode(address(token), 18));
// Mock the token balance
vm.mockCall(
address(token), abi.encodeWithSelector(token.balanceOf.selector, address(optimismPortal)), abi.encode(0)
);
// Call minimumGasLimit(0) before vm.expectRevert to ensure vm.expectRevert is for depositERC20Transaction
uint64 gasLimit = optimismPortal2.minimumGasLimit(0);
vm.expectRevert(TransferFailed.selector);
// Deposit the token into the portal
optimismPortal2.depositERC20Transaction(address(1), 100, 0, gasLimit, false, "");
}
/// @dev Tests that `depositERC20Transaction` reverts when creating a contract with a non-zero target.
function test_depositERC20Transaction_isCreationNotZeroTarget_reverts() external {
// Mock the gas paying token to be the ERC20 token
vm.mockCall(address(systemConfig), abi.encodeWithSignature("gasPayingToken()"), abi.encode(address(token), 18));
// Call minimumGasLimit(0) before vm.expectRevert to ensure vm.expectRevert is for depositERC20Transaction
uint64 gasLimit = optimismPortal2.minimumGasLimit(0);
vm.expectRevert(BadTarget.selector);
// Deposit the token into the portal
optimismPortal2.depositERC20Transaction(address(1), 0, 0, gasLimit, true, "");
}
/// @dev Tests that `depositERC20Transaction` reverts when the gas limit is too low.
function test_depositERC20Transaction_gasLimitTooLow_reverts() external {
// Mock the gas paying token to be the ERC20 token
vm.mockCall(address(systemConfig), abi.encodeWithSignature("gasPayingToken()"), abi.encode(address(token), 18));
vm.expectRevert(SmallGasLimit.selector);
// Deposit the token into the portal
optimismPortal2.depositERC20Transaction(address(0), 0, 0, 0, false, "");
}
/// @dev Tests that `depositERC20Transaction` reverts when the data is too large.
function test_depositERC20Transaction_dataTooLarge_reverts() external {
bytes memory data = new bytes(120_001);
data[120_000] = 0x01;
// Mock the gas paying token to be the ERC20 token
vm.mockCall(address(systemConfig), abi.encodeWithSignature("gasPayingToken()"), abi.encode(address(token), 18));
uint64 gasLimit = optimismPortal2.minimumGasLimit(120_001);
vm.expectRevert(LargeCalldata.selector);
// Deposit the token into the portal
optimismPortal2.depositERC20Transaction(address(0), 0, 0, gasLimit, false, data);
}
/// @dev Tests that `balance()` returns the correct balance when the gas paying token is not ether.
function testFuzz_balance_nonEther_succeeds(uint256 _amount) external {
// Mint the token to the contract and approve the token for the portal
token.mint(address(this), _amount);
token.approve(address(optimismPortal2), _amount);
// Mock the gas paying token to be the ERC20 token
vm.mockCall(address(systemConfig), abi.encodeWithSignature("gasPayingToken()"), abi.encode(address(token), 18));
// Deposit the token into the portal
optimismPortal2.depositERC20Transaction(address(0), _amount, 0, optimismPortal.minimumGasLimit(0), false, "");
// Check that the balance has been correctly updated
assertEq(optimismPortal2.balance(), _amount);
}
/// @dev Tests that `finalizeWithdrawalTransaction` succeeds.
function test_finalizeWithdrawalTransaction_provenWithdrawalHash_nonEther_succeeds() external {
// Mint the token to the contract and approve the token for the portal
token.mint(address(this), _defaultTx.value);
token.approve(address(optimismPortal2), _defaultTx.value);
// Mock the gas paying token to be the ERC20 token
vm.mockCall(address(systemConfig), abi.encodeWithSignature("gasPayingToken()"), abi.encode(address(token), 18));
// Deposit the token into the portal
optimismPortal2.depositERC20Transaction(
address(bob), _defaultTx.value, 0, optimismPortal.minimumGasLimit(0), false, ""
);
assertEq(optimismPortal2.balance(), _defaultTx.value);
vm.expectEmit(address(optimismPortal2));
emit WithdrawalProven(_withdrawalHash, alice, bob);
optimismPortal2.proveWithdrawalTransaction({
_tx: _defaultTx,
_disputeGameIndex: _proposedGameIndex,
_outputRootProof: _outputRootProof,
_withdrawalProof: _withdrawalProof
});
// Warp past the finalization period.
game.resolveClaim(0, 0);
game.resolve();
vm.warp(block.timestamp + optimismPortal2.proofMaturityDelaySeconds() + 1);
vm.expectEmit(address(optimismPortal2));
emit WithdrawalFinalized(_withdrawalHash, true);
vm.expectCall(_defaultTx.target, 0, _defaultTx.data);
vm.expectCall(
address(token), 0, abi.encodeWithSelector(token.transfer.selector, _defaultTx.target, _defaultTx.value)
);
optimismPortal2.finalizeWithdrawalTransaction(_defaultTx);
assertEq(optimismPortal2.balance(), 0);
assertEq(token.balanceOf(address(bob)), 100);
}
/// @dev Helper for depositing a transaction.
function depositTransaction(
address _from,
address _to,
uint256 _value,
uint64 _gasLimit,
bool _isCreation,
bytes memory _data
)
internal
{
if (_isCreation) {
_to = address(0);
}
vm.assume(_data.length <= 120_000);
ResourceMetering.ResourceConfig memory rcfg = systemConfig.resourceConfig();
_gasLimit =
uint64(bound(_gasLimit, optimismPortal2.minimumGasLimit(uint64(_data.length)), rcfg.maxResourceLimit));
// Mock the gas paying token to be the ERC20 token
vm.mockCall(address(systemConfig), abi.encodeWithSignature("gasPayingToken()"), abi.encode(address(token), 18));
bytes memory opaqueData = abi.encodePacked(uint256(0), _value, _gasLimit, _isCreation, _data);
vm.expectEmit(address(optimismPortal2));
emit TransactionDeposited(
_from, // from
_to,
uint256(0), // DEPOSIT_VERSION
opaqueData
);
// Deposit the token into the portal
optimismPortal2.depositTransaction(_to, _value, _gasLimit, _isCreation, _data);
// Assert final balance equals the deposited amount
assertEq(token.balanceOf(address(optimismPortal2)), 0);
assertEq(optimismPortal2.balance(), 0);
}
/// @dev Tests that `depositTransaction` succeeds when a custom gas token is used but the msg.value is zero.
function testFuzz_depositTransaction_customGasToken_noValue_senderIsOrigin_succeeds(
address _to,
uint256 _value,
uint64 _gasLimit,
bool _isCreation,
bytes memory _data
)
external
{
// Ensure that msg.sender == tx.origin
vm.startPrank(address(this), address(this));
depositTransaction({
_from: address(this),
_to: _to,
_value: _value,
_gasLimit: _gasLimit,
_isCreation: _isCreation,
_data: _data
});
}
/// @dev Tests that `depositTransaction` succeeds when a custom gas token is used but the msg.value is zero.
function testFuzz_depositTransaction_customGasToken_noValue_senderNotOrigin_succeeds(
address _to,
uint256 _value,
uint64 _gasLimit,
bool _isCreation,
bytes memory _data
)
external
{
// Ensure that msg.sender != tx.origin
vm.startPrank(address(this), address(1));
depositTransaction({
_from: AddressAliasHelper.applyL1ToL2Alias(address(this)),
_to: _to,
_value: _value,
_gasLimit: _gasLimit,
_isCreation: _isCreation,
_data: _data
});
}
/// @dev Tests that `depositTransaction` fails when a custom gas token is used and msg.value is non-zero.
function test_depositTransaction_customGasToken_withValue_reverts() external {
// Mock the gas paying token to be the ERC20 token
vm.mockCall(address(systemConfig), abi.encodeWithSignature("gasPayingToken()"), abi.encode(address(token), 18));
vm.expectRevert(NoValue.selector);
// Deposit the token into the portal
optimismPortal2.depositTransaction{ value: 100 }(address(0), 0, 0, false, "");
}
}
......@@ -348,6 +348,12 @@ contract Specification_Test is CommonTest {
_addSpec({ _name: "OptimismPortal2", _sel: _getSel("respectedGameTypeUpdatedAt()") });
_addSpec({ _name: "OptimismPortal2", _sel: _getSel("proofSubmitters(bytes32,uint256)") });
_addSpec({ _name: "OptimismPortal2", _sel: _getSel("numProofSubmitters(bytes32)") });
_addSpec({ _name: "OptimismPortal2", _sel: _getSel("balance()") });
_addSpec({
_name: "OptimismPortal2",
_sel: _getSel("depositERC20Transaction(address,uint256,uint256,uint64,bool,bytes)")
});
_addSpec({ _name: "OptimismPortal2", _sel: _getSel("setGasPayingToken(address,uint8,bytes32,bytes32)") });
// ProtocolVersions
_addSpec({ _name: "ProtocolVersions", _sel: _getSel("RECOMMENDED_SLOT()") });
......
......@@ -31,7 +31,7 @@ contract DeploymentSummary is DeploymentSummaryCode {
address internal constant optimismMintableERC20FactoryAddress = 0x39Aea2Dd53f2d01c15877aCc2791af6BDD7aD567;
address internal constant optimismMintableERC20FactoryProxyAddress = 0x20A42a5a785622c6Ba2576B2D6e924aA82BFA11D;
address internal constant optimismPortalAddress = 0xbdD90485FCbcac869D5b5752179815a3103d8131;
address internal constant optimismPortal2Address = 0xfcbb237388CaF5b08175C9927a37aB6450acd535;
address internal constant optimismPortal2Address = 0x542e5F5d3934b6A8A8B4219cbc99D3D87a7137E1;
address internal constant optimismPortalProxyAddress = 0x978e3286EB805934215a88694d80b09aDed68D90;
address internal constant preimageOracleAddress = 0x3bd7E801E51d48c5d94Ea68e8B801DFFC275De75;
address internal constant protocolVersionsAddress = 0xfbfD64a6C0257F613feFCe050Aa30ecC3E3d7C3F;
......
This source diff could not be displayed because it is too large. You can view the blob instead.
......@@ -31,7 +31,7 @@ contract DeploymentSummaryFaultProofs is DeploymentSummaryFaultProofsCode {
address internal constant optimismMintableERC20FactoryAddress = 0x39Aea2Dd53f2d01c15877aCc2791af6BDD7aD567;
address internal constant optimismMintableERC20FactoryProxyAddress = 0x20A42a5a785622c6Ba2576B2D6e924aA82BFA11D;
address internal constant optimismPortalAddress = 0xbdD90485FCbcac869D5b5752179815a3103d8131;
address internal constant optimismPortal2Address = 0xfcbb237388CaF5b08175C9927a37aB6450acd535;
address internal constant optimismPortal2Address = 0x542e5F5d3934b6A8A8B4219cbc99D3D87a7137E1;
address internal constant optimismPortalProxyAddress = 0x978e3286EB805934215a88694d80b09aDed68D90;
address internal constant preimageOracleAddress = 0x3bd7E801E51d48c5d94Ea68e8B801DFFC275De75;
address internal constant protocolVersionsAddress = 0xfbfD64a6C0257F613feFCe050Aa30ecC3E3d7C3F;
......@@ -431,7 +431,7 @@ contract DeploymentSummaryFaultProofs is DeploymentSummaryFaultProofsCode {
value = hex"0000000000000000000000000000000000000000000000000000000000000003";
vm.store(systemOwnerSafeAddress, slot, value);
slot = hex"360894a13ba1a3210667c828492db98dca3e2076cc3735a920a3ca505d382bbc";
value = hex"000000000000000000000000fcbb237388caf5b08175c9927a37ab6450acd535";
value = hex"000000000000000000000000542e5f5d3934b6a8a8b4219cbc99d3d87a7137e1";
vm.store(optimismPortalProxyAddress, slot, value);
slot = hex"0000000000000000000000000000000000000000000000000000000000000000";
value = hex"0000000000000000000000000000000000000000000000000000000000000001";
......
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