├── .gas-snapshot ├── .github └── workflows │ └── test.yml ├── .gitignore ├── .gitmodules ├── README.md ├── foundry.toml ├── src └── testing │ └── OpStackStd.sol └── test ├── AddressAliasHelper.sol └── OpStackStd.t.sol /.gas-snapshot: -------------------------------------------------------------------------------- 1 | OpStackStdTest:testCreatesContractOnL2() (gas: 292621) 2 | OpStackStdTest:testParseOpaqueData(uint256,uint256,uint64,bool,bytes) (runs: 256, μ: 28449, ~: 22862) 3 | OpStackStdTest:testRelaysContractCallOnL2() (gas: 213376) 4 | OpStackStdTest:testSendEthOnL2() (gas: 98225) -------------------------------------------------------------------------------- /.github/workflows/test.yml: -------------------------------------------------------------------------------- 1 | name: test 2 | 3 | on: workflow_dispatch 4 | 5 | env: 6 | FOUNDRY_PROFILE: ci 7 | 8 | jobs: 9 | check: 10 | strategy: 11 | fail-fast: true 12 | 13 | name: Foundry project 14 | runs-on: ubuntu-latest 15 | steps: 16 | - uses: actions/checkout@v3 17 | with: 18 | submodules: recursive 19 | 20 | - name: Install Foundry 21 | uses: foundry-rs/foundry-toolchain@v1 22 | with: 23 | version: nightly 24 | 25 | - name: Run Forge build 26 | run: | 27 | forge --version 28 | forge build --sizes 29 | id: build 30 | 31 | - name: Run Forge tests 32 | run: | 33 | forge test -vvv 34 | id: test 35 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Compiler files 2 | cache/ 3 | out/ 4 | 5 | # Ignores development broadcast logs 6 | !/broadcast 7 | /broadcast/*/31337/ 8 | /broadcast/**/dry-run/ 9 | 10 | # Docs 11 | docs/ 12 | 13 | # Dotenv file 14 | .env 15 | 16 | .vscode 17 | -------------------------------------------------------------------------------- /.gitmodules: -------------------------------------------------------------------------------- 1 | [submodule "lib/forge-std"] 2 | path = lib/forge-std 3 | url = https://github.com/foundry-rs/forge-std 4 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | ### Op-Forge 2 | 3 | Tools for using forge with OpStack chains. 4 | 5 | #### OPStackStd - Testing Tools 6 | 7 | ##### relayDepositTransaction 8 | A helper method for creating an L2 transactions from the L1 event logs, just as the Op-Node does. 9 | 10 | ```solidity 11 | import {OpStackStd} from "Op-Forge/testing/OpStackStd.sol"; 12 | 13 | function testSendEthOnL2() public { 14 | address bob = address(0xb0b); 15 | // pre check 16 | vm.selectFork(opStackFork); 17 | assertEq(bob.balance, 0); 18 | 19 | vm.selectFork(l1Fork); 20 | uint256 value = 1e18; 21 | vm.deal(address(this), value); 22 | vm.recordLogs(); 23 | portal.depositTransaction{value: value}({_to: bob, _value: value, _gasLimit: 21_000, _isCreation: false, _data: ""}); 24 | VmSafe.Log[] memory logs = vm.getRecordedLogs(); 25 | OpStackStd.relayDepositTransaction(logs, opStackFork); 26 | 27 | // post check 28 | vm.selectFork(opStackFork); 29 | assertEq(bob.balance, value); 30 | } 31 | ``` -------------------------------------------------------------------------------- /foundry.toml: -------------------------------------------------------------------------------- 1 | [profile.default] 2 | src = "src" 3 | out = "out" 4 | libs = ["lib"] 5 | 6 | # See more config options https://github.com/foundry-rs/foundry/blob/master/crates/config/README.md#all-options 7 | -------------------------------------------------------------------------------- /src/testing/OpStackStd.sol: -------------------------------------------------------------------------------- 1 | // SPDX-License-Identifier: MIT 2 | pragma solidity ^0.8.0; 3 | 4 | import {Vm, VmSafe} from "forge-std/Vm.sol"; 5 | 6 | library OpStackStd { 7 | struct DepositTransaction { 8 | // not sure we can do source hash because 9 | // we need to know the relative position of the given logs 10 | // in the whole block. Could just assume 0? But probably not important 11 | // for most testing 12 | // 13 | // bytes32 sourceHash, 14 | // 15 | address from; 16 | address to; 17 | uint256 value; 18 | uint256 mint; 19 | uint64 gasLimit; 20 | bool isCreation; 21 | bytes data; 22 | } 23 | 24 | Vm private constant vm = Vm(address(uint160(uint256(keccak256("hevm cheat code"))))); 25 | bytes32 constant transactionDepositedTopic = keccak256("TransactionDeposited(address,address,uint256,bytes)"); 26 | 27 | error DepositTransactionNotFound(); 28 | error OpaqueDataTooShort(); 29 | 30 | function relayDepositTransaction(VmSafe.Log[] memory logs, uint256 forkId) public { 31 | uint256 currentFork = vm.activeFork(); 32 | DepositTransaction memory depositTx = getDepositTransactionFromLogs(logs); 33 | vm.selectFork(forkId); 34 | vm.deal(depositTx.from, depositTx.mint); 35 | vm.startPrank(depositTx.from); 36 | if (depositTx.to == address(0)) { 37 | address newContract; 38 | uint256 value = depositTx.value; 39 | bytes memory bytecode = depositTx.data; 40 | // TODO(Wilson): How can we limit the gas here? 41 | assembly { 42 | newContract := create(value, add(bytecode, 0x20), mload(bytecode)) 43 | } 44 | 45 | if (newContract == address(0)) { 46 | revert("L2 contract creation failed"); 47 | } 48 | } else { 49 | (bool success, bytes memory returnedData) = 50 | depositTx.to.call{value: depositTx.value, gas: depositTx.gasLimit}(depositTx.data); 51 | 52 | if (!success) { 53 | if (returnedData.length > 0) { 54 | (string memory reason) = abi.decode(returnedData, (string)); 55 | revert(string.concat("L2 call reverted with reason: ", reason)); 56 | } else { 57 | revert("L2 call reverted with no reason"); 58 | } 59 | } 60 | } 61 | vm.selectFork(currentFork); 62 | } 63 | 64 | function getDepositTransactionFromLogs(VmSafe.Log[] memory logs) public pure returns (DepositTransaction memory) { 65 | for (uint256 i; i < logs.length; i++) { 66 | if (logs[i].topics[0] == transactionDepositedTopic) { 67 | address from = address(bytes20(logs[i].topics[1] << 96)); 68 | address to = address(bytes20(logs[i].topics[2] << 96)); 69 | (bytes memory opaqueData) = abi.decode(logs[i].data, (bytes)); 70 | (uint256 mint, uint256 value, uint64 gas, bool isCreation, bytes memory data) = 71 | parseOpaqueData(opaqueData); 72 | return DepositTransaction({ 73 | from: from, 74 | to: isCreation ? address(0) : to, 75 | value: value, 76 | mint: mint, 77 | gasLimit: gas, 78 | isCreation: isCreation, 79 | data: data 80 | }); 81 | } 82 | } 83 | 84 | revert DepositTransactionNotFound(); 85 | } 86 | 87 | function parseOpaqueData(bytes memory opaqueData) 88 | public 89 | pure 90 | returns (uint256 mint, uint256 value, uint64 _gas, bool isCreation, bytes memory data) 91 | { 92 | if (opaqueData.length < 73) revert OpaqueDataTooShort(); 93 | 94 | // Extract the mint value (first 32 bytes) 95 | assembly { 96 | mint := mload(add(opaqueData, 32)) 97 | } 98 | 99 | // Extract the value (second 32 bytes) 100 | assembly { 101 | value := mload(add(opaqueData, 64)) 102 | } 103 | 104 | assembly { 105 | _gas := mload(add(opaqueData, 72)) // Load next 32 bytes 106 | } 107 | 108 | // Extract isCreation (next 1 byte) 109 | uint256 isCreationRaw = uint8(opaqueData[72]); 110 | isCreation = isCreationRaw == 0x01; 111 | 112 | // The rest of the data is the dynamic `data` part 113 | if (opaqueData.length > 73) { 114 | data = new bytes(opaqueData.length - 73); 115 | for (uint256 i = 73; i < opaqueData.length; i++) { 116 | data[i - 73] = opaqueData[i]; 117 | } 118 | } 119 | } 120 | } 121 | -------------------------------------------------------------------------------- /test/AddressAliasHelper.sol: -------------------------------------------------------------------------------- 1 | // SPDX-License-Identifier: Apache-2.0 2 | 3 | /* 4 | * Copyright 2019-2021, Offchain Labs, Inc. 5 | * 6 | * Licensed under the Apache License, Version 2.0 (the "License"); 7 | * you may not use this file except in compliance with the License. 8 | * You may obtain a copy of the License at 9 | * 10 | * http://www.apache.org/licenses/LICENSE-2.0 11 | * 12 | * Unless required by applicable law or agreed to in writing, software 13 | * distributed under the License is distributed on an "AS IS" BASIS, 14 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 15 | * See the License for the specific language governing permissions and 16 | * limitations under the License. 17 | */ 18 | 19 | pragma solidity ^0.8.0; 20 | 21 | library AddressAliasHelper { 22 | uint160 constant offset = uint160(0x1111000000000000000000000000000000001111); 23 | 24 | /// @notice Utility function converts the address that submitted a tx 25 | /// to the inbox on L1 to the msg.sender viewed on L2 26 | /// @param l1Address the address in the L1 that triggered the tx to L2 27 | /// @return l2Address L2 address as viewed in msg.sender 28 | function applyL1ToL2Alias(address l1Address) internal pure returns (address l2Address) { 29 | unchecked { 30 | l2Address = address(uint160(l1Address) + offset); 31 | } 32 | } 33 | 34 | /// @notice Utility function that converts the msg.sender viewed on L2 to the 35 | /// address that submitted a tx to the inbox on L1 36 | /// @param l2Address L2 address as viewed in msg.sender 37 | /// @return l1Address the address in the L1 that triggered the tx to L2 38 | function undoL1ToL2Alias(address l2Address) internal pure returns (address l1Address) { 39 | unchecked { 40 | l1Address = address(uint160(l2Address) - offset); 41 | } 42 | } 43 | } 44 | -------------------------------------------------------------------------------- /test/OpStackStd.t.sol: -------------------------------------------------------------------------------- 1 | // SPDX-License-Identifier: MIT 2 | pragma solidity ^0.8.0; 3 | 4 | import {Test, console2} from "forge-std/Test.sol"; 5 | import {Vm, VmSafe} from "lib/forge-std/src/Vm.sol"; 6 | 7 | import {OpStackStd} from "../src/testing/OpStackStd.sol"; 8 | import {AddressAliasHelper} from "./AddressAliasHelper.sol"; 9 | 10 | interface IPortal { 11 | event TransactionDeposited(address indexed from, address indexed to, uint256 indexed version, bytes opaqueData); 12 | 13 | function depositTransaction(address _to, uint256 _value, uint64 _gasLimit, bool _isCreation, bytes memory _data) 14 | external 15 | payable; 16 | } 17 | 18 | contract Simple { 19 | uint256 public x = 1; 20 | } 21 | 22 | contract OpStackStdTest is Test { 23 | uint256 l1Fork; 24 | uint256 opStackFork; 25 | IPortal portal; 26 | 27 | function setUp() public { 28 | l1Fork = vm.createFork("https://ethereum.publicnode.com"); 29 | opStackFork = vm.createFork("https://mainnet.base.org"); 30 | portal = IPortal(0x49048044D57e1C92A77f79988d21Fa8fAF74E97e); 31 | 32 | vm.selectFork(l1Fork); 33 | } 34 | 35 | // TODO(Wilson) 36 | // function getDepositTransactionFromLogs 37 | // fails if no log 38 | // correctly if log 39 | // handles create address correctly 40 | // 41 | // function relayDepositTransaction 42 | // reverts with revert message 43 | // reverts without revert message 44 | 45 | function testRelaysContractCallOnL2() public { 46 | uint256 value = 0.02002 ether; 47 | address to = 0xb129419F9B035E9d80B4a320ffcf5BE93Cb7994B; 48 | uint64 gasLimit = 300_000; 49 | bool isCreation = false; 50 | bytes memory data = 51 | hex"173a562d00000000000000000000000000000000000000000000000000000000000000010000000000000000000000003a6372b2013f9876a84761187d933dee0653e3770000000000000000000000000000000000000000000000000000000000000007"; 52 | vm.deal(address(this), value); 53 | vm.recordLogs(); 54 | portal.depositTransaction{value: value}(to, value, gasLimit, isCreation, data); 55 | VmSafe.Log[] memory logs = vm.getRecordedLogs(); 56 | vm.expectCall(to, value, gasLimit, data); 57 | OpStackStd.relayDepositTransaction(logs, opStackFork); 58 | } 59 | 60 | function testSendEthOnL2() public { 61 | address bob = address(0xb0b); 62 | // pre check 63 | vm.selectFork(opStackFork); 64 | assertEq(bob.balance, 0); 65 | 66 | vm.selectFork(l1Fork); 67 | uint256 value = 1e18; 68 | vm.deal(address(this), value); 69 | vm.recordLogs(); 70 | portal.depositTransaction{value: value}({ 71 | _to: bob, 72 | _value: value, 73 | _gasLimit: 21_000, 74 | _isCreation: false, 75 | _data: "" 76 | }); 77 | VmSafe.Log[] memory logs = vm.getRecordedLogs(); 78 | OpStackStd.relayDepositTransaction(logs, opStackFork); 79 | 80 | // post check 81 | vm.selectFork(opStackFork); 82 | assertEq(bob.balance, value); 83 | } 84 | 85 | function testCreatesContractOnL2() public { 86 | address expectedAddress = computeCreateAddress(AddressAliasHelper.applyL1ToL2Alias(address(this)), 0); 87 | bytes memory expectedBytes = hex""; 88 | assertEq(expectedAddress.code, expectedBytes); 89 | vm.recordLogs(); 90 | portal.depositTransaction(address(0), 0, 300_000, true, type(Simple).creationCode); 91 | VmSafe.Log[] memory logs = vm.getRecordedLogs(); 92 | OpStackStd.relayDepositTransaction(logs, opStackFork); 93 | 94 | vm.selectFork(opStackFork); 95 | Simple simple = new Simple(); 96 | assertEq(expectedAddress.code, address(simple).code); 97 | } 98 | 99 | function testParseOpaqueData(uint256 mint, uint256 value, uint64 gasLimit, bool isCreation, bytes memory data) 100 | public 101 | { 102 | bytes memory opaque = abi.encodePacked(mint, value, gasLimit, isCreation, data); 103 | (uint256 mint_, uint256 value_, uint64 gas_, bool isCreation_, bytes memory data_) = 104 | OpStackStd.parseOpaqueData(opaque); 105 | assertEq(mint_, mint); 106 | assertEq(value_, value); 107 | assertEq(gas_, gasLimit); 108 | assertEq(isCreation_, isCreation); 109 | assertEq(data_, data); 110 | } 111 | } 112 | --------------------------------------------------------------------------------