Snapshot
- Focus: Automated transaction simulation with human readable calldata and state change verification.
- Context: Matter Labs needed to eliminate calldata generation errors in operational wallet transactions and provide their security team with verifiable, human readable simulation outputs before execution.
- Who it's for: Matter Labs.
Outcomes
- Integrated Tenderly simulation API into the tool, producing shareable simulation links.
- Built an EVM trace decoder that transforms raw opcode execution into human readable storage diffs and event logs, resolving proxy contracts, mapping keys, and nested storage layouts.
- Added configurable fork state controls (block number pinning, absolute and relative timestamp overrides) for deterministic simulation of time sensitive transactions.
Context
ZKsync's governance infrastructure involves multi stage protocol upgrades that span Ethereum L1 and multiple zkEVM L2 chains. These upgrades are executed through a series of transactions: governance votes are initiated on L2, relayed as L2→L1 messages, and then executed on L1 through the ProtocolUpgradeHandler contract via specially crafted transactions. A single malformed transaction in this pipeline can stall an upgrade across the entire ecosystem, requiring a new governance vote cycle to recover.
Matter Labs had already built the transaction-simulator as the single source of truth for blockchain transactions executed from operational wallets, but the simulation output was difficult to interpret and lacked human readable verification of state changes. The CI pipeline ensured transactions were valid and simulated before merging. Our engagement focused on extending this tool with Tenderly-based verification, human-readable simulation output, and flexible fork configuration.
The Challenge
The tool already handled transaction simulation and validation. Our scope was adding Tenderly integration, human readable event and storage change logging, and fork configuration controls. The core engineering difficulties came from the storage decoder, which required working at the EVM opcode level to turn raw execution traces into something reviewers could actually read.
- Recovering human readable field names from raw storage slots: EVM storage is flat: a SSTORE gives you a slot address and a value, nothing else. Mapping that back to something like upgradeStatus[0x5678].executed requires reconstructing the full Solidity storage layout, handling proxy contracts where DELEGATECALL means the storage belongs to one address but the layout comes from another, and tracing KECCAK256 operations backwards through the execution trace to recover mapping keys that are otherwise lost.
- Identifying contracts without verified source code: Some contracts in the simulation don't have verified source on block explorers, so the decoder can't just fetch ABIs externally. We needed a matching strategy that compares deployed bytecode against known compiled artifacts, falling back to function selector and event topic overlap scoring when bytecode doesn't match exactly.
- Maintaining feature parity across Tenderly and local forks: Tenderly doesn't support zkEVM chains, and local fork tools (Anvil, anvil-zksync) have different RPC semantics and quirks. Features like timestamp overrides and block pinning needed to work consistently across both environments, which meant building separate code paths that produce equivalent simulation behavior.
Our Approach
We identified this as a transaction observability problem. The simulator already caught reverts, but reviewers had no way to verify that a successful transaction actually did the right thing. A transaction that doesn't revert can still write the wrong value to the wrong storage slot.
We addressed this across three areas:
- Tenderly as an independent verification layer: Rather than replacing the local fork simulation, Tenderly runs alongside it, giving the security team a shareable, externally hosted trace without requiring local tooling.
- An opcode level decoder for storage and events: Instead of relying on external indexers or verified source code, the decoder ships with a registry of known ABIs and matches contracts by bytecode similarity at simulation time. This keeps the tool self contained and deterministic.
- Fork configuration co-located with transaction data: Block number pinning and timestamp overrides live in the same JSON file as the transactions they affect, keeping state assumptions explicit and version controlled.
Execution
Phase 1: Tenderly Integration
We implemented a TenderlySimulator class that wraps Tenderly's bundle simulation API. The simulator transforms internal Transaction objects into Tenderly payloads, handles gas price resolution (using on chain fee data or the optional maxFeePerGas override), and batches simulations per transaction file.
const simulator = new TenderlySimulator(
process.env.TENDERLY_ACCOUNT,
process.env.TENDERLY_PROJECT,
process.env.TENDERLY_ACCESS_KEY
);
const links = await simulator.simulateTransactions(transactionBatches, providers);
saveTenderlySimulationLinks(links);zkEVM chains are automatically excluded since Tenderly does not support them. The CI workflow stores and manages the necessary secrets to interact with Tenderly and generate the simulations. Simulation links are saved as CI artifacts, making them easily available for review and audit.
Phase 2: Event and Storage Change Decoding
The decoder works through the following pipeline:

- The contract metadata resolver compares on chain bytecode against known compiled contracts using a composite similarity score: bytecode comparison, function selector overlap, and event topic hash matching.
- The slot to field matcher builds all possible field paths from a contract's storage layout, handling inplace encoding (simple fields, structs), mapping encoding (by tracing KECCAK256 operations backwards through the execution trace to recover original keys), and bytes/string encoding for dynamic types. For proxy contracts using DELEGATECALL, storage changes are correctly attributed to the proxy address while using the implementation's storage layout for decoding.
Example decoded output:
Call ProtocolUpgradeHandler: 0x1234...abcd @ mainnet
├─ Storage write
│ ├─ Field: ProtocolUpgradeHandler.upgradeStatus[0x5678].executed
│ ├─ Old Value: false
│ ├─ New Value: true
│ ├─ Slot: 0xabc123...
├─ Event emit
│ ├─ UpgradeExecuted(upgradeId: |0x5678|, executor: |0x9999|)
└──
└─ ReturnPhase 3: Fork Configuration and Timestamp Control
We extended the transaction JSON schema to support fork level state pinning and per transaction time manipulation. Fork parameters (blockNumber, timestamp) and per transaction overrides (absoluteTimestamp, deltaTimestamp) live in the same JSON file as the transactions they affect. Both work with local Anvil/anvil-zksync forks and Tenderly simulations, maintaining feature parity across execution environments.
Results
The transaction simulator is actively used by the Matter Labs security team for transaction simulation and verification before execution.
The CI pipeline catches calldata generation errors, schema violations, and simulation failures before they reach execution. Transactions that would have reverted on chain are caught at the PR stage.
The human readable storage diff decoder eliminated the need for manual hex inspection of governance transaction side effects. Reviewers can verify that an upgrade sets the correct fields to the correct values without tracing storage slots by hand.
Tenderly integration provides an independent, externally hosted verification layer. The security team can share simulation links with stakeholders who do not have local tooling set up.
This engagement extended the existing simulation tool into a verification system where reviewers can see exactly what a transaction does before it executes. The work continues to support Matter Labs' security processes as the ZKsync protocol evolves.
