diff --git a/src/doc/simulate-l2-deposit-transactions.md b/src/doc/simulate-l2-deposit-transactions.md index 1fe1462b4f..2280e9e0b7 100644 --- a/src/doc/simulate-l2-deposit-transactions.md +++ b/src/doc/simulate-l2-deposit-transactions.md @@ -1,18 +1,20 @@ # Simulating L2 Deposit Transactions with Integration Tests -The following steps describe how to automatically simulate L2 deposit transactions prior to L1 task execution using integration tests. This approach is similar to the [manual Tenderly simulation approach](./simulate-l2-ownership-transfer.md), with the key difference being that it uses a local supersim instance and automated transaction replay instead of manual Tenderly simulation. +The following steps describe how to automatically simulate L2 deposit transactions prior to L1 task execution using integration tests. This approach is based on the [manual Tenderly simulation approach](./simulate-l2-ownership-transfer.md), with the difference that it uses a local supersim instance and automated transaction replay instead of manual Tenderly simulation. ## Overview -When executing L1 transactions that trigger L2 deposit transactions (via the OptimismPortal), we can gain additional confidence by automatically replaying these deposit transactions on a local L2 fork, simulating what op-node does. The `IntegrationBase` contract provides a `_relayAllMessages` function that: +When executing L1 transactions that trigger L2 deposit transactions (via OptimismPortal), we can gain additional confidence by automatically replaying these deposit transactions on local L2 forks, simulating what op-node does. The `IntegrationBase` contract provides a `_relayAllMessages` function that: 1. Extracts all `TransactionDeposited` events from the L1 execution -2. Decodes the deposit transaction parameters -3. Executes each transaction on the L2 fork with the correct aliased sender -4. Generates Tenderly simulation links for each transaction +2. Filters events by portal address to ensure only relevant events are relayed to each L2 +3. Decodes the deposit transaction parameters +4. Executes each transaction on the corresponding L2 fork(s) with the correct sender 5. Asserts that all transactions succeed -This automated approach is particularly useful for complex tasks that emit multiple deposit transactions, such as the revenue share upgrade path which can emit 12+ deposit transactions per execution. +This automated approach is particularly useful for: +- Complex tasks that emit multiple deposit transactions (e.g., revenue share upgrades with 12+ transactions per chain) +- Multi-chain deployments where the same L1 transaction affects multiple L2s ## Prerequisites @@ -22,16 +24,19 @@ You'll need to run supersim with forked chains to test against real network stat Install supersim if you haven't already: - https://github.com/ethereum-optimism/supersim +https://github.com/ethereum-optimism/supersim + +Start supersim with forked chains for multiple L2s: -Start supersim with forked chains: ```bash -supersim fork --chains=op +supersim fork --chains=op,ink ``` -**Note:** You can use any L2 chain supported by supersim (e.g., `op`, `base`, `mode`, etc.). The default ports are: +**Note:** You can specify any L2 chains supported by supersim (e.g., `op`, `base`, `mode`, `ink`, etc.). The default ports are: - L1 (Ethereum): `http://127.0.0.1:8545` - L2 (OP Mainnet): `http://127.0.0.1:9545` +- L2 (Ink Mainnet): `http://127.0.0.1:9546` +- Additional L2s will increment the port (9547, 9548, etc.) For different L2 chains, adjust the RPC URLs and network IDs accordingly. @@ -53,12 +58,18 @@ contract YourIntegrationTest is IntegrationBase { // Fork IDs uint256 internal _mainnetForkId; - uint256 internal _l2ForkId; + uint256 internal _opMainnetForkId; + uint256 internal _inkMainnetForkId; + + // Portal addresses (L1) + address internal constant OP_MAINNET_PORTAL = 0xbEb5Fc579115071764c7423A4f12eDde41f106Ed; + address internal constant INK_MAINNET_PORTAL = 0x5d66C1782664115999C47c9fA5cd031f495D3e4F; function setUp() public { // Create forks pointing to supersim instances _mainnetForkId = vm.createFork("http://127.0.0.1:8545"); - _l2ForkId = vm.createFork("http://127.0.0.1:9545"); + _opMainnetForkId = vm.createFork("http://127.0.0.1:9545"); + _inkMainnetForkId = vm.createFork("http://127.0.0.1:9546"); // Deploy template on L1 fork vm.selectFork(_mainnetForkId); @@ -69,25 +80,37 @@ contract YourIntegrationTest is IntegrationBase { ### Step 2: Execute L1 Transaction and Relay Messages -In your test function, execute the L1 transaction while recording logs, then relay all deposit messages to L2: +In your test function, execute the L1 transaction while recording logs, then relay all deposit messages to multiple L2s: ```solidity function test_yourTask_integration() public { string memory _configPath = "path/to/your/config.toml"; - // Step 1: Execute L1 transaction recording logs + // Step 1: Record logs for L1→L2 message replay vm.recordLogs(); - template.simulate(_configPath, new address[](0)); - // Step 2: Relay messages from L1 to L2 + // Step 2: Execute task simulation + template.simulate(_configPath); + + // Step 3: Relay deposit transactions from L1 to all L2s + uint256[] memory forkIds = new uint256[](2); + forkIds[0] = _opMainnetForkId; + forkIds[1] = _inkMainnetForkId; + + address[] memory portals = new address[](2); + portals[0] = OP_MAINNET_PORTAL; + portals[1] = INK_MAINNET_PORTAL; + // Pass true for _isSimulate since simulate() emits events twice // (once during dry-run validation, once during actual simulation) - _relayAllMessages(_l2ForkId, true); + _relayAllMessages(forkIds, true, portals); - // Step 3: Assert the state of the L2 contracts - string memory _config = vm.readFile(_configPath); + // Step 4: Assert the state of each L2 chain + vm.selectFork(_opMainnetForkId); + // Add OP Mainnet assertions here... - // Add your L2 state assertions here... + vm.selectFork(_inkMainnetForkId); + // Add Ink Mainnet assertions here... } ``` @@ -111,79 +134,78 @@ assertEq( ## Example: Revenue Share Integration Test -See [RevenueShareIntegration.t.sol](../../test/integration/RevenueShareIntegration.t.sol) for a complete example that: +See [RevShareContractsUpgraderIntegration.t.sol](../../test/integration/RevShareContractsUpgraderIntegration.t.sol) for a complete example that: -- Tests opt-in scenarios +- Tests multi-chain deployments (OP Mainnet and Ink Mainnet simultaneously) - Validates multiple L2 contracts (L1Withdrawer, RevShareCalculator, FeeSplitter, FeeVaults) - Asserts complex state relationships between contracts +- Uses portal filtering to ensure correct event routing Key test structure: + ```solidity -function test_optInRevenueShare_integration() public { - // 1. Execute L1 transaction +function test_upgradeAndSetupRevShare_integration() public { + // Step 1: Record logs for L1→L2 message replay vm.recordLogs(); - revenueShareTemplate.simulate(_configPath, new address[](0)); - // 2. Relay messages to L2 - _relayAllMessages(_l2ForkId, true); + // Step 2: Execute task simulation + revShareTask.simulate("test/tasks/example/eth/016-revshare-upgrade-and-setup/config.toml"); + + // Step 3: Relay deposit transactions from L1 to all L2s + uint256[] memory forkIds = new uint256[](2); + forkIds[0] = _opMainnetForkId; + forkIds[1] = _inkMainnetForkId; + + address[] memory portals = new address[](2); + portals[0] = OP_MAINNET_PORTAL; + portals[1] = INK_MAINNET_PORTAL; - // 3. Assert L2 state - assertEq(IL1Withdrawer(L1_WITHDRAWER).minWithdrawalAmount(), expectedValue); - assertEq(IFeeSplitter(FEE_SPLITTER).sharesCalculator(), REV_SHARE_CALCULATOR); - // ... more assertions + _relayAllMessages(forkIds, IS_SIMULATE, portals); + + // Step 4: Assert the state of the OP Mainnet contracts + vm.selectFork(_opMainnetForkId); + _assertL2State(OP_L1_WITHDRAWER, OP_REV_SHARE_CALCULATOR, ...); + + // Step 5: Assert the state of the Ink Mainnet contracts + vm.selectFork(_inkMainnetForkId); + _assertL2State(INK_L1_WITHDRAWER, INK_REV_SHARE_CALCULATOR, ...); } ``` ## Understanding the Output -When you run an integration test, `_relayAllMessages` will output: +When you run an integration test, `_relayAllMessages` will output for each L2: ``` ================================================================================ -=== Replaying Deposit Transactions on L2 === -=== Each transaction includes Tenderly simulation link === -=== Network is set to 10 (OP Mainnet) - adjust if testing on different L2 === +=== Relaying Deposit Transactions on L2 === +=== Portal: 0xbEb5Fc579115071764c7423A4f12eDde41f106Ed +=== Network is set to 10 === ================================================================================ -Tenderly Simulation Link for transaction #1 -https://dashboard.tenderly.co/TENDERLY_USERNAME/TENDERLY_PROJECT/simulator/new?network=10&contractAddress=0x...&from=0x...&gas=656536&value=0&rawFunctionInput=0x... - -Tenderly Simulation Link for transaction #2 -... - === Summary === -Total transactions: 12 -Successful transactions: 12 +Total transactions processed: 11 +Successful transactions: 11 Failed transactions: 0 ``` -## Manual Tenderly Simulation +This output repeats for each L2 chain being tested, with different portal addresses and chain IDs. -While integration tests provide automated validation, you can also manually simulate individual transactions in Tenderly by: +## Portal Filtering -1. Copying a Tenderly link from the integration test output -2. Opening the link in your browser -3. Inspecting the transaction details, state changes, and gas usage +The `_relayAllMessages` function filters events by portal address to ensure that only deposit transactions meant for a specific L2 are relayed to that chain. This is critical for multi-chain deployments where: -For detailed manual simulation steps, see [simulate-l2-ownership-transfer.md](./simulate-l2-ownership-transfer.md). +1. A single L1 transaction may emit deposit transactions to multiple L2 chains +2. Each L2 should only receive transactions deposited through its corresponding portal +3. The portal address identifies which OptimismPortal emitted the `TransactionDeposited` event -## Recording Simulation Results +For example: +- Events from `0xbEb5Fc579115071764c7423A4f12eDde41f106Ed` (OP Mainnet Portal) → OP Mainnet fork +- Events from `0x5d66C1782664115999C47c9fA5cd031f495D3e4F` (Ink Mainnet Portal) → Ink Mainnet fork -After running integration tests and generating Tenderly simulations, document the results in your task's validation file. See [RevenueShareSimulations.md](../../test/integration/tenderly/RevenueShareSimulations.md) for an example format: - -```markdown -# Your Task Simulations - -### Scenario 1 -1. [Contract Deploy](https://www.tdly.co/shared/simulation/...) Gas: 558,056/656,536 (85%) -2. [Contract Upgrade](https://www.tdly.co/shared/simulation/...) Gas: 65,138/150,000 (43%) -... -``` +This filtering prevents cross-contamination of deposit transactions between chains. ## Troubleshooting -### Failed Tenderly Transactions but working in the Supersim integration -Something that sucks on the Tenderly simulations is that it is very hard (actually I don't know how) to keep the state changes of previously simulated transactions. So if you are testing the transactions of a task that first -> upgrades a contract to have setters and then -> calls that setter, when simulating the setter transaction, it will revert. - ### Fork Issues When first running the fork test against supersim, do it with a `--match-test` that does only one fork for caching the network states. If you try to run more than one at the same time by, for example, using `--match-contract`, you might get timeout issues diff --git a/test/integration/IntegrationBase.t.sol b/test/integration/IntegrationBase.t.sol index 3f81e2024d..669490e4f3 100644 --- a/test/integration/IntegrationBase.t.sol +++ b/test/integration/IntegrationBase.t.sol @@ -44,8 +44,7 @@ abstract contract IntegrationBase is Test { console2.log("================================================================================"); console2.log("=== Relaying Deposit Transactions on L2 ==="); console2.log("=== Portal:", _portal); - console2.log("=== Each transaction includes Tenderly simulation link ==="); - console2.log("=== Network is set to", block.chainid, "- adjust if testing on different L2 ==="); + console2.log("=== Network is set to", block.chainid); console2.log("================================================================================"); // If this is a simulation, only take the second half of logs to avoid processing duplicates @@ -78,7 +77,7 @@ abstract contract IntegrationBase is Test { _transactionCount++; // Process and execute the transaction - bool _success = _processDepositTransaction(_from, _to, _opaqueData, _transactionCount); + bool _success = _processDepositTransaction(_from, _to, _opaqueData); if (_success) { _successCount++; @@ -99,7 +98,7 @@ abstract contract IntegrationBase is Test { } /// @notice Process and execute a deposit transaction - function _processDepositTransaction(address _from, address _to, bytes memory _opaqueData, uint256 _txNumber) + function _processDepositTransaction(address _from, address _to, bytes memory _opaqueData) internal returns (bool) { @@ -112,9 +111,6 @@ abstract contract IntegrationBase is Test { // Extract data (bytes 73 onwards) bytes memory _data = _slice(_opaqueData, 73, _opaqueData.length - 73); - // Print Tenderly simulation parameters - _logTransactionDetails(_from, _to, _value, _gasLimit, _data, _txNumber); - // Execute the transaction on L2 as if it came from the aliased address vm.prank(_from); (bool _success,) = _to.call{value: _value, gas: _gasLimit}(_data); @@ -122,28 +118,6 @@ abstract contract IntegrationBase is Test { return _success; } - /// @notice Log transaction details and Tenderly link - function _logTransactionDetails( - address _from, - address _to, - uint256 _value, - uint64 _gasLimit, - bytes memory _data, - uint256 _txNumber - ) internal pure { - if (_data.length >= 4) { - bytes4 _selector; - assembly { - _selector := mload(add(_data, 32)) - } - } - - // Generate Tenderly simulation link - string memory _tenderlyLink = _generateTenderlyLink(_to, _from, uint256(_gasLimit), _value, _data); - console2.log("\nTenderly Simulation Link for transaction #", _txNumber); - console2.log(_tenderlyLink); - } - /// @notice Helper function to slice bytes function _slice(bytes memory _data, uint256 _start, uint256 _length) internal pure returns (bytes memory) { bytes memory _result = new bytes(_length); @@ -152,63 +126,4 @@ abstract contract IntegrationBase is Test { } return _result; } - - /// @notice Generate Tenderly simulation link for L2 transaction - function _generateTenderlyLink( - address _contractAddress, - address _from, - uint256 _gas, - uint256 _value, - bytes memory _rawFunctionInput - ) internal pure returns (string memory) { - // Convert bytes to hex string - string memory _calldataHex = _bytesToHexString(_rawFunctionInput); - - // Build the Tenderly URL - // network=10 for OP Mainnet (change if testing on different L2) - return string.concat( - "https://dashboard.tenderly.co/TENDERLY_USERNAME/TENDERLY_PROJECT/simulator/new", - "?network=10", - "&contractAddress=0x", - _toAsciiString(_contractAddress), - "&from=0x", - _toAsciiString(_from), - "&gas=", - vm.toString(_gas), - "&value=", - vm.toString(_value), - "&rawFunctionInput=0x", - _calldataHex - ); - } - - /// @notice Convert address to lowercase hex string without 0x prefix - function _toAsciiString(address _addr) internal pure returns (string memory) { - bytes memory _s = new bytes(40); - for (uint256 _i; _i < 20; _i++) { - bytes1 _b = bytes1(uint8(uint256(uint160(_addr)) / (2 ** (8 * (19 - _i))))); - bytes1 _hi = bytes1(uint8(_b) / 16); - bytes1 _lo = bytes1(uint8(_b) - 16 * uint8(_hi)); - _s[2 * _i] = _char(_hi); - _s[2 * _i + 1] = _char(_lo); - } - return string(_s); - } - - /// @notice Convert bytes to hex string without 0x prefix - function _bytesToHexString(bytes memory _data) internal pure returns (string memory) { - bytes memory _hexChars = "0123456789abcdef"; - bytes memory _result = new bytes(_data.length * 2); - for (uint256 _i; _i < _data.length; _i++) { - _result[_i * 2] = _hexChars[uint8(_data[_i] >> 4)]; - _result[_i * 2 + 1] = _hexChars[uint8(_data[_i] & 0x0f)]; - } - return string(_result); - } - - /// @notice Convert nibble to hex character - function _char(bytes1 _b) internal pure returns (bytes1) { - if (uint8(_b) < 10) return bytes1(uint8(_b) + 0x30); - else return bytes1(uint8(_b) + 0x57); - } } diff --git a/test/integration/tenderly/RevenueShareSimulations.md b/test/integration/tenderly/RevenueShareSimulations.md deleted file mode 100644 index 6c2c736e60..0000000000 --- a/test/integration/tenderly/RevenueShareSimulations.md +++ /dev/null @@ -1,28 +0,0 @@ -# Revenue Share Simulations -A list of tenderly simulations are provided based on the formatting of the `TransactionDeposited` events emitted in L1 and its execution in the target L2, in this case, OP Mainnet. For details, check the [Simulating Portal Deposit Transactions](todo) document - -### Revenue Share Upgrade Opt In Default Calculator -1. [L1Withdrawer Deploy](https://www.tdly.co/shared/simulation/cfb6f065-1f37-47ee-96e1-9a32ea80f540) Gas: 558,056/656,536 (85%) -2. [Rev Share Calculator Deploy](https://www.tdly.co/shared/simulation/5c813d81-9fa7-45e5-ac61-bbe89b917be8) Gas: 579,688/681,986 (85%) -3. [Fee Splitter Deploy](https://www.tdly.co/shared/simulation/93b43e8d-e64c-4ce5-ad2c-7047e91e4a4c) Gas: 1,121,747/1,319,702 (85%) -4. [Fee Splitter Upgrade](https://www.tdly.co/shared/simulation/ac7c91dc-9c1b-4b56-bfd4-bad52fd0b2aa) Gas: 65,138/150,000 (43%) -5. [Operator Fee Vault Deploy](https://www.tdly.co/shared/simulation/4c95b834-89b4-4f2e-8bca-8df9ed0cc885) Gas: 893,030/1,200,000 (74%) -6. [Operator Fee Vault Upgrade](https://www.tdly.co/shared/simulation/45662e37-ab84-4ad9-96ee-46144eb0ec49) Gas: 48,770/150,000 (33%) -7. [Sequencer Fee Vault Deploy](https://www.tdly.co/shared/simulation/4e24dd89-f601-4a54-a72d-3ac4a4c1e7c8) Gas: 890,692/1,200,000 (74%) -8. [Sequencer Fee Vault Upgrade](https://www.tdly.co/shared/simulation/29051fa4-0687-4a58-9ea5-6a09497e3846) Gas: 46,270/150,000 (31%) -9. [Base Fee Vault Deploy](https://www.tdly.co/shared/simulation/515d5554-1f3e-4061-be24-6d3e888ed4d9) Gas: 890,692/1,200,000 (74%) -10. [Base Fee Vault Upgrade](https://www.tdly.co/shared/simulation/04d8de5e-89bc-419d-bbcf-ee0448d0e813) Gas: 48,782/150,000 (33%) -11. [L1 Fee Vault Deploy](https://www.tdly.co/shared/simulation/bcee51db-b549-4c05-811f-be831e6a1aa0) Gas: 890,704/1,200,000 (74%) -12. [L1 Fee Vault Upgrade](https://www.tdly.co/shared/simulation/20dbe19b-cea7-4542-a73b-71934394153e) Gas: 48,782/150,000 (33%) - -### Revenue Share Upgrade Opt In Custom Calculator -1. [Fee Splitter Deploy](https://www.tdly.co/shared/simulation/8d8c15a1-12d5-4d07-a1bb-8e336032e19d) Gas: 1,121,747/1,319,702 (85%) -2. [Fee Splitter Upgrade](https://www.tdly.co/shared/simulation/ac7c91dc-9c1b-4b56-bfd4-bad52fd0b2aa) Gas: 65,138/150,000 (43%) -3. [Operator Fee Vault Deploy](https://www.tdly.co/shared/simulation/4c95b834-89b4-4f2e-8bca-8df9ed0cc885) Gas: 893,030/1,200,000 (74%) -4. [Operator Fee Vault Upgrade](https://www.tdly.co/shared/simulation/45662e37-ab84-4ad9-96ee-46144eb0ec49) Gas: 48,770/150,000 (33%) -5. [Sequencer Fee Vault Deploy](https://www.tdly.co/shared/simulation/4e24dd89-f601-4a54-a72d-3ac4a4c1e7c8) Gas: 890,692/1,200,000 (74%) -6. [Sequencer Fee Vault Upgrade](https://www.tdly.co/shared/simulation/29051fa4-0687-4a58-9ea5-6a09497e3846) Gas: 46,270/150,000 (31%) -7. [Base Fee Vault Deploy](https://www.tdly.co/shared/simulation/515d5554-1f3e-4061-be24-6d3e888ed4d9) Gas: 890,692/1,200,000 (74%) -8. [Base Fee Vault Upgrade](https://www.tdly.co/shared/simulation/04d8de5e-89bc-419d-bbcf-ee0448d0e813) Gas: 48,782/150,000 (33%) -9. [L1 Fee Vault Deploy](https://www.tdly.co/shared/simulation/bcee51db-b549-4c05-811f-be831e6a1aa0) Gas: 890,704/1,200,000 (74%) -10. [L1 Fee Vault Upgrade](https://www.tdly.co/shared/simulation/20dbe19b-cea7-4542-a73b-71934394153e) Gas: 48,782/150,000 (33%)