Skip to content
Logo

Debugging

Forge provides detailed traces and an interactive debugger to understand contract execution.

Traces

Run tests with -vvvv to see full execution traces:

$ forge test -vvvv
Compiling...
No files changed, compilation skipped
Ran 1 test for test/OwnerUpOnly.t.sol:OwnerUpOnlyTest
[PASS] test_IncrementAsOwner() (gas: 59372)
Traces:
  [59372] OwnerUpOnlyTest::test_IncrementAsOwner()
    ├─ [2407] OwnerUpOnly::count() [staticcall]
    │   └─ ← [Return] 0
    ├─ [43524] OwnerUpOnly::increment()
    │   └─ ← [Stop]
    ├─ [2407] OwnerUpOnly::count() [staticcall]
    │   └─ ← [Return] 1
    └─ ← [Stop]
Suite result: ok. 1 passed; 0 failed; 0 skipped; finished in 390.68µs (89.41µs CPU time)
Ran 1 test suite in 5.96ms (390.68µs CPU time): 1 tests passed, 0 failed, 0 skipped (1 total tests)

The trace shows every call, its inputs, outputs, and gas usage.

Understanding trace output

Each line shows:

  • Gas used in brackets
  • Contract::function being called
  • Call type (staticcall, delegatecall, etc.)
  • Return value or revert reason

Indentation indicates call depth.

Tracing a failed transaction

Debug a transaction that failed on-chain:

$ cast run 0x<txhash> --rpc-url $RPC_URL

This replays the transaction and shows the execution trace.

Interactive debugger

Launch the debugger for a specific test:

$ forge test --debug test_Increment

The debugger shows:

  • Source code with current line highlighted
  • Stack contents
  • Memory contents
  • Storage changes
  • Call stack

Debugger commands

KeyAction
nStep to next opcode
sStep into call
oStep out of call
gGo to start
GGo to end
cContinue to next breakpoint
qQuit
hShow help

Debugging scripts

Debug a script:

$ forge script script/Deploy.s.sol --debug

Console logging

Add logs to your contracts for debugging:

import {console} from "forge-std/console.sol";
 
function transfer(address to, uint256 amount) public {
    console.log("Transfer from:", msg.sender);
    console.log("Transfer to:", to);
    console.log("Amount:", amount);
    // ...
}

View logs with -vv or higher:

$ forge test -vv

For structured output, Foundry also supports console.table, which can make repeated values easier to scan than a long sequence of console.log lines.

Labeling addresses

Make traces more readable by labeling addresses:

function setUp() public {
    alice = makeAddr("alice");
    bob = makeAddr("bob");
    
    vm.label(address(token), "Token");
    vm.label(address(pool), "Pool");
}

Traces will show Token::transfer() instead of 0x1234...::transfer().

Stack traces

When a test fails, use -vvv to see a stack trace showing exactly where the revert occurred:

$ forge test -vvv
Solc 0.8.10 finished in 570.06ms
Compiler run successful!
Ran 1 test for test/FailingTest.t.sol:VaultTest
[FAIL: Unauthorized()] test_WithdrawAsNotOwner() (gas: 8418)
Traces:
  [8418] VaultTest::test_WithdrawAsNotOwner()
    ├─ [0] VM::prank(ECRecover: [0x0000000000000000000000000000000000000001])
    │   └─ ← [Return]
    ├─ [191] Vault::withdraw() [staticcall]
    │   └─ ← [Revert] Unauthorized()
    └─ ← [Revert] Unauthorized()
Backtrace:
  at Vault.withdraw
  at VaultTest.test_WithdrawAsNotOwner
Suite result: FAILED. 0 passed; 1 failed; 0 skipped; finished in 253.98µs (47.88µs CPU time)
Ran 1 test suite in 7.01ms (253.98µs CPU time): 0 tests passed, 1 failed, 0 skipped (1 total tests)

The trace shows the call hierarchy with the revert bubbling up, and the Backtrace pinpoints the exact location in your code.

Inspecting inheritance linearization

When debugging overrides in a multiple-inheritance hierarchy, inspect the method-resolution order directly:

$ forge inspect src/MyContract.sol:MyContract linearization

This shows the order Solidity uses to resolve inherited functions, which is often the fastest way to understand why a particular override or super call is being selected.