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 -vvvvCompiling...
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_URLThis replays the transaction and shows the execution trace.
Interactive debugger
Launch the debugger for a specific test:
$ forge test --debug test_IncrementThe debugger shows:
- Source code with current line highlighted
- Stack contents
- Memory contents
- Storage changes
- Call stack
Debugger commands
| Key | Action |
|---|---|
n | Step to next opcode |
s | Step into call |
o | Step out of call |
g | Go to start |
G | Go to end |
c | Continue to next breakpoint |
q | Quit |
h | Show help |
Debugging scripts
Debug a script:
$ forge script script/Deploy.s.sol --debugConsole 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 -vvFor 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 -vvvSolc 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 linearizationThis 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.
Was this helpful?
