Skip to content

Latest commit

 

History

History
340 lines (262 loc) · 12 KB

File metadata and controls

340 lines (262 loc) · 12 KB

Fork Testing Guide

This guide explains how to use the transaction simulation framework to debug and analyze transactions on forked networks.

Overview

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

Quick Start

1. Start Anvil Fork

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 19000000

Important: The --fork-block-number flag ensures you're testing against the exact state when the transaction occurred/will occur.

2. Run Tests

# 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

Architecture

┌─────────────────────────────────────────────────────────────────┐
│                        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 │  │
│  └─────────────────────┘      └─────────────────────────────┘  │
└─────────────────────────────────────────────────────────────────┘

Creating a New Test

1. Create Test File

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");
    }
}

2. Using Raw Calldata

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);
}

Error Decoding Flow

When a transaction reverts, the framework automatically:

  1. Captures all contracts involved in the call via vm.stopAndReturnStateDiff()
  2. Fetches ABIs from Etherscan V2 for each contract
  3. Matches the error selector against error definitions in each ABI
  4. 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       │
└───────────────────────┘

Example Output

--- 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

Configuration

Environment Variables

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

Supported Chains

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

ABI Caching

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/

TxSimulator API Reference

Core Functions

Function Description
simulateTx(txn) Basic simulation, returns result
simulateTxWithContracts(txn) Simulation + captures all involved contracts
simulateTxWithRecording(txn) Simulation + logs storage changes

Logging Functions

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

Error Decoding Functions

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

Helper Functions

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)

Troubleshooting

"Could not instantiate forked environment"

Anvil is not running. Start it with:

anvil --fork-url YOUR_RPC_URL --fork-block-number BLOCK_NUMBER

"Warning: No API key set"

Add your Etherscan API key to .env:

ETHERSCANV2_VERIFY_API_KEY=your_key_here

ABI fetch returns empty

  • 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)

Error shows "Known error database" instead of ABI

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

Example: Debugging a Failed Transaction

  1. Get transaction details from block explorer:

    • Target contract: 0xd1DE...
    • From: 0xfE38...
    • Block: 41323464
    • Input data: 0xba087652...
  2. Start anvil at that block:

    anvil --fork-url https://base-mainnet.g.alchemy.com/v2/KEY --fork-block-number 41323464
  3. Create test file with the transaction details

  4. Run test:

    yarn test_forked --match-test test_my_transaction -vvvv
  5. Analyze output:

    • Check decoded error message
    • Review involved contracts
    • Examine pre/post state differences
    • Identify root cause from parameter values