Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
148 changes: 85 additions & 63 deletions src/doc/simulate-l2-deposit-transactions.md
Original file line number Diff line number Diff line change
@@ -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

Expand All @@ -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.

Expand All @@ -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);
Expand All @@ -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...
}
```

Expand All @@ -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
91 changes: 3 additions & 88 deletions test/integration/IntegrationBase.t.sol
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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++;
Expand All @@ -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)
{
Expand All @@ -112,38 +111,13 @@ 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);

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);
Expand All @@ -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);
}
}
Loading