Debugging Contracts with Foundry

What is Foundry

Foundry is a Solidity Framework for building, testing, fuzzing, debugging and deploying Solidity smart contracts. It can manage dependencies, compile your project, run tests, deploy contracts, and lets you interact with the chain from the command-line and via Solidity scripts.Foundry comes with the following tools that can be used to debugging:

  • Anvil: Used to deploy a local Ethereum node

  • Cast: Used to send RPC calls and interact with the chain

  • Forge: Used to build, compile, and test smart contracts

You can install Foundry by running the following commands:

Linux/Mac:

curl -L https://foundry.paradigm.xyz | bash
foundryup

Windows: (Requires Rust, install from https://rustup.rs/)

cargo install --git https://github.com/foundry-rs/foundry --bins --locked

Docker:

docker pull ghcr.io/foundry-rs/foundry:latest

More detailed instructions on installation available here:

https://book.getfoundry.sh/getting-started/installation

Anvil

Anvil deploys a local Ethereum node in your environment that can be used to deploy a node. Simply run anvil to deploy one.

It has a few useful properties. It allows you to fork a network at the latest, or any specific height. You can run the following command to fork the Mantle mainnet environment for debugging your code:

export RPC_URL="https://rpc.mantle.xyz"
anvil --fork-url $RPC_URL

Call traces are more detailed, i.e., we can see which functions are being executed in remote contracts, i.e., the functions that executed by every contract.

Note that it isn't necessary to use an Anvil node to use Foundry. You can use other local nodes like Hardhat, Ganache, etc. as well.

Cast

You can use the following cast commands to interact with the chain.

The examples below are based on Anvil. If you have Anvil up and running, try invoking the following cases in another command line window. This will allow you to debug online contracts using the mantle mainnet.

cast call / cast send

cast send can be used to sign and publish a transaction on the chain.You can use it make an arbitrary contract method call, like so:

cast send $contract_address "someFunc(unit256)" 0x... \--private-key $wallet_private_key

cast call on the other hand can be used to perform an account call without publishing it to the chain, and that's why you don't need to specify a private key.So for instance, you can make a contract method call, like so:

cast call $contract_address "someFunc(uint, args[])" $number "[arg_1, ... , arg_n]"

export WMNT=0x78c1b0C915c4FAA5FffA6CAbf0219DA63d7f4cb8
cast call $WMNT "balanceOf(address)" 0x1858d52cf57c07A018171D7a1E68DC081F17144f
//0x00000000000000000000000000000000000000000006cfd1854e3ec57a5d636d
cast --to-dec 0x00000000000000000000000000000000000000000006cfd1854e3ec57a5d636d
//8234949754837270014026605

cast run

cast run can be used to run a published transaction in a local environment and print the trace. For example:

cast run $TX_HASH

cast run 0xf500a3c8e08960aa28ccdd557730087b1d9b618a42b9595c7800ceeff53dddaa

You can also replay transactions in the debugger using the --debug flag, like so:

cast run --debug $TX_HASH

Taking this deposit transaction as an example:

cast run 0x16a3d14f54fc36097184b2b774b0d30e35e03e033805eb3180a0ee8d4c5427ef --debug

Learn more about the debugger interface here: https://book.getfoundry.sh/forge/debugger

cast block / cast tx

cast block can be used to fetch block information from the chain. Running cast block returns block info for the latest block.

cast tx can be used to fetch transaction information from the chain using the transaction hash. The command structure is as follows:

cast tx $TX_HASH

cast rpc

cast rpc can be used to send a raw JSON-RPC request to a node.Since Mantle Network supports Ethereum's JSON-RPC interface, you can use the following command to call eth_getBlockByNumber and fetch information for the latest block:

cast rpc --rpc-url https://rpc.mantle.xyz/ eth_getBlockByNumber "latest" "false"

cast storage & cast index

cast storage can be used to fetch the raw value of a contract's storage slot. cast-index can compute the storage slot location for an entry in a mapping.The cast index calculates the storage location based on the KEY_TYPE, KEY, and SLOT_NUMBER.For example, to fetch the value of slot 0 for a contract:

cast storage $contract_address 0

We'll take the balance of account 0x1858d52cf57c07A018171D7a1E68DC081F17144f about $WMNT as an example, which can be obtained in two ways:

  • Conventional function call method This has already been implemented in the aforementioned cast call.

  • Reading contract slot storage method First, based on the WMNT source code, it's determined that the balanceOf state variable is located at the 0th slot with KEY_TYPE being 'address'.

contract WMANTLE is ERC20  {
    constructor() ERC20("Wrapped Mantle", "WMNT") {}
    event  Deposit(address indexed dst, uint256 wad);
    event  Withdrawal(address indexed src, uint256 wad);
...

contract ERC20 is Context, IERC20, IERC20Metadata {
    mapping(address => uint256) private _balances;
    mapping(address => mapping(address => uint256)) private _allowances;
    uint256 private _totalSupply;
    string private _name;
    string private _symbol;
...
# For KEY 0x1858d52cf57c07A018171D7a1E68DC081F17144f and slot 0, the corresponding storage location.
cast index address 0x1858d52cf57c07A018171D7a1E68DC081F17144f 0
//0x2edcadc9f197c279f92ccdbf8b108a59937d545e881935fccd2cb58923bc739b

# Retrieve the raw data from the corresponding storage location, converting from address to integer, address=>int
cast storage $WMNT 0x2edcadc9f197c279f92ccdbf8b108a59937d545e881935fccd2cb58923bc739b
0x00000000000000000000000000000000000000000006cfd1854e3ec57a5d636d

cast abi-decode / cast abi-encode

cast abi-decode can be used to decode any ABI-encoded data. For example, to decode the output data for a balanceOf call:

cast abi-decode "balanceOf(address)(uint256)" \0x000000000000000000000000000000000000000000000000000000000000000a

cast abi-encode can be used to ABI encode any given function arguments, excluding the selector. For example, to ABI-encode the arguments for a function call:

cast abi-encode "someFunc(address,uint256)" 0x... 1

forge

5.1 Initialize the project.-forge init

forge init <dir_name>forge init —template <template_path> <dir_name>

View the current directory structure.

(base) ➜ tree -L 2
.
└── hello_foundry
    ├── foundry.toml
    ├── lib
    ├── script
    ├── src
    └── test

5.2 forge build

The corresponding compile command is

cd hello_foundry
forge build
forge build -w

Typically, two panes are opened in tmux. The first pane is used to view real-time coding status, monitoring in real-time with the -w option. In the second pane, code is written. After each code modification, once saved, the first pane will display in real-time whether the compilation has passed.

5.3 Automated testing-forge test

# You can print logs using the -v level, -vv level, and -vvv level.
forge test -v /-vv / -vvv

# Use -w for watch mode.
forge test -v /-vv / -vvv -w

For more detailed procedures on testing, please refer to. https://book.getfoundry.sh/reference/forge/forge-test

Forge debug

Forge ships with an interactive debugger.

forge debug --debug $FILE --sig $FUNC

In the newly initialized project, we can enter the following command to enable the interactive debugger.

forge debug --debug Counter --sig "setNumber(uint256)" 5

If you want to debug on the forked mainnet, you can enter the following command.

forge debug --debug Counter --sig "setNumber(uint256)" 5 -f http://127.0.0.1:8545

or

forge debug --debug Counter --sig "setNumber(uint256)" 5 --fork-url https://rpc.mantle.xyz

breakpoint

Places a breakpoint to jump to in the debugger view.Calling vm.breakpoint('<char>, true) is equivalent to vm.breakpoint('<char>), but calling vm.breakpoint('<char, false) will erase the breakpoint at '<char.If the char is overwritten, only the last one that was visited in the execution steps is considered.

Example

function testBreakpoint() public {
    vm.breakpoint("a");
}

Opening up the debugger in a test environment and pressing 'a will then place the debugger step at the place where the breakpoint cheatcode was called.

By integrating Anvil and Cast, you can fork and test by interacting with contracts on the live network.

Last updated