This guide explains how to use the transaction simulation framework to debug and analyze transactions on forked networks.
The fork testing framework allows you to:
- Simulate transactions at specific historical block numbers
- Automatically decode custom errors from contract ABIs
- Trace through complex multi-contract calls
- Understand why transactions fail before they hit mainnet
Start a local fork of the target network at a specific block:
# Base network example
anvil --fork-url https://base-mainnet.g.alchemy.com/v2/YOUR_API_KEY --fork-block-number 41323464
# Ethereum mainnet example
anvil --fork-url https://eth-mainnet.g.alchemy.com/v2/YOUR_API_KEY --fork-block-number 19000000Important: The --fork-block-number flag ensures you're testing against the exact state when the transaction occurred/will occur.
# Run all fork tests
yarn test_forked
# Run specific test contract
yarn test_forked --match-contract YearnRedeemTest
# Run with verbose output (recommended for debugging)
yarn test_forked --match-contract YearnRedeemTest -vvvv┌─────────────────────────────────────────────────────────────────┐
│ Your Test File │
│ (e.g., YearnRedeemTest.sol) │
└─────────────────────────┬───────────────────────────────────────┘
│ extends
▼
┌─────────────────────────────────────────────────────────────────┐
│ TxSimulator.sol │
│ ┌─────────────────┐ ┌──────────────────┐ ┌───────────────┐ │
│ │ simulateTx() │ │ logPostState() │ │ decodeError() │ │
│ │ simulateTxWith │ │ logPreState() │ │ fetchAbi() │ │
│ │ Contracts() │ │ logVaultState() │ │ │ │
│ └─────────────────┘ └──────────────────┘ └───────┬───────┘ │
└─────────────────────────────────────────────────────┼───────────┘
│ FFI
┌───────────────────────────┘
▼
┌─────────────────────────────────────────────────────────────────┐
│ Shell Scripts │
│ ┌─────────────────────┐ ┌─────────────────────────────┐ │
│ │ fetch_abi.sh │ │ decode_error.sh │ │
│ │ - Etherscan V2 API │ │ - Dynamic ABI lookup │ │
│ │ - Multi-chain │ │ - Known error fallback │ │
│ │ - Caching │ │ - 4byte.directory fallback │ │
│ └─────────────────────┘ └─────────────────────────────┘ │
└─────────────────────────────────────────────────────────────────┘
Create a new file in tests_fork/:
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.0;
import { TxSimulator } from "./TxSimulator.sol";
import "forge-std/console.sol";
contract MyTransactionTest is TxSimulator {
// Target contract
address constant TARGET = 0x...;
// Transaction sender
address constant USER = 0x...;
function setUp() public {
setupChainConfig(); // Required for ABI fetching
console.log("Chain ID:", block.chainid);
console.log("Block Number:", block.number);
}
function test_my_transaction() public {
// Build the transaction
SimTx memory txn = SimTx({
to: TARGET,
from: USER,
data: abi.encodeWithSelector(...), // Or use raw hex
value: 0
});
// Log pre-state
logPreState(txn);
// Execute with contract tracking (enables dynamic ABI lookup)
(SimResult memory result, address[] memory involvedContracts) =
simulateTxWithContracts(txn);
// Log post-state with error decoding
logPostStateWithContracts(txn, result, involvedContracts);
// Assert expected outcome
assertTrue(result.success, "Transaction should succeed");
}
}If you have raw transaction data (e.g., from a failed transaction):
function test_with_raw_calldata() public {
// Paste the raw calldata from the transaction
bytes memory callData = hex"ba087652000000000000000000000000...";
SimTx memory txn = SimTx({
to: TARGET,
from: USER,
data: callData,
value: 0
});
(SimResult memory result, address[] memory involvedContracts) =
simulateTxWithContracts(txn);
logPostStateWithContracts(txn, result, involvedContracts);
}When a transaction reverts, the framework automatically:
- Captures all contracts involved in the call via
vm.stopAndReturnStateDiff() - Fetches ABIs from Etherscan V2 for each contract
- Matches the error selector against error definitions in each ABI
- Decodes parameters with correct names from the originating contract
Transaction Reverts
│
▼
┌───────────────────────┐
│ Capture involved │
│ contracts (6 found) │
└───────────┬───────────┘
│
▼
┌───────────────────────┐ ┌─────────────────────┐
│ Try ABI lookup for │────►│ Etherscan V2 API │
│ each contract │ │ (with caching) │
└───────────┬───────────┘ └─────────────────────┘
│
▼
┌───────────────────────┐
│ Found error in token │
│ contract ABI! │
│ ERC20InsufficientBal │
└───────────┬───────────┘
│
▼
┌───────────────────────┐
│ Decode with param │
│ names: sender, │
│ balance, needed │
└───────────────────────┘
--- REVERT ERROR DECODED ---
Error: ERC20InsufficientBalance
Signature: ERC20InsufficientBalance(address,uint256,uint256)
Source: ABI from contract 0x696F9436B67233384889472Cd7cD58A6fB5DF4f1
Decoded Parameters:
sender: 0x4Bd3c99d0a79821558701DD8Bda72F362DE3765b
balance: 868935690887062754 [8.689e17]
needed: 2159618703324599343739 [2.159e21]
Contracts involved in call: 6
- 0xd1DEfB01f9A2aa1299d05689f56574B6D7d6d9F2
- 0xd8063123BBA3B480569244AE66BFE72B6c84b00d
- 0x696F9436B67233384889472Cd7cD58A6fB5DF4f1 ← Error originated here
- 0x4Bd3c99d0a79821558701DD8Bda72F362DE3765b
- 0xd18a95B265b8eD3DEcB145814ee8f4F20d7878B4
Create a .env file with your API keys:
# Etherscan V2 unified API key (works for all chains)
ETHERSCANV2_VERIFY_API_KEY=your_api_key_here
# Alternative: chain-specific keys (fallback)
ETHERSCAN_API_KEY=your_key
BASESCAN_API_KEY=your_key
ARBISCAN_API_KEY=your_key| Chain | Chain ID | Notes |
|---|---|---|
| Ethereum | 1 | Mainnet |
| Base | 8453 | L2 |
| Arbitrum | 42161 | L2 |
| Optimism | 10 | L2 |
| Polygon | 137 | Sidechain |
| Avalanche | 43114 | C-Chain |
| BNB Chain | 56 | BSC |
Fetched ABIs are cached in ./cache/abi/ to avoid repeated API calls:
cache/abi/
├── 8453_0x696f9436b67233384889472cd7cd58a6fb5df4f1.json
├── 8453_0xd1defb01f9a2aa1299d05689f56574b6d7d6d9f2.json
└── ...
To clear cache: rm -rf ./cache/abi/
| Function | Description |
|---|---|
simulateTx(txn) |
Basic simulation, returns result |
simulateTxWithContracts(txn) |
Simulation + captures all involved contracts |
simulateTxWithRecording(txn) |
Simulation + logs storage changes |
| Function | Description |
|---|---|
logPreState(txn) |
Logs chain info, tx details, balances |
logPostState(txn, result) |
Logs result, decoded errors, balances |
logPostStateWithContracts(txn, result, contracts) |
Enhanced error decoding with all contracts |
logVaultState(vault, user) |
ERC4626 vault-specific state |
logTokenState(token, accounts) |
ERC20 token balances |
| Function | Description |
|---|---|
decodeError(errorData) |
Basic decoding (known errors + 4byte) |
decodeErrorWithContext(errorData, contract) |
Decoding with single contract ABI |
decodeErrorWithContracts(errorData, contracts) |
Decoding with multiple contract ABIs |
fetchAbi(target) |
Fetch ABI from Etherscan |
| Function | Description |
|---|---|
createTx(to, from, data) |
Create SimTx struct |
createTxWithValue(to, from, data, value) |
Create SimTx with ETH value |
fundEth(account, amount) |
Give ETH to an address |
fundToken(token, account, amount, slot) |
Give ERC20 tokens (requires balance slot) |
Anvil is not running. Start it with:
anvil --fork-url YOUR_RPC_URL --fork-block-number BLOCK_NUMBERAdd your Etherscan API key to .env:
ETHERSCANV2_VERIFY_API_KEY=your_key_here- Contract may not be verified on Etherscan
- Try clearing cache:
rm -rf ./cache/abi/ - Check if the contract is a proxy (fetch implementation ABI instead)
The error signature wasn't found in any of the involved contracts' ABIs. This can happen when:
- The contract isn't verified
- The error is from a library or inherited contract not in the ABI
- The fallback to known errors still provides accurate decoding
-
Get transaction details from block explorer:
- Target contract:
0xd1DE... - From:
0xfE38... - Block:
41323464 - Input data:
0xba087652...
- Target contract:
-
Start anvil at that block:
anvil --fork-url https://base-mainnet.g.alchemy.com/v2/KEY --fork-block-number 41323464
-
Create test file with the transaction details
-
Run test:
yarn test_forked --match-test test_my_transaction -vvvv
-
Analyze output:
- Check decoded error message
- Review involved contracts
- Examine pre/post state differences
- Identify root cause from parameter values