BONUS: Cheatcodes in Foundry

BONUS: Cheatcodes in Foundry

A guide that covers some of the many cheatcodes available in Foundry

Introduction

For testing complex smart contracts, simply examining their outputs may not be enough. To manipulate the state of the blockchain, as well as test for specific reverts and events, Foundry is shipped with a set of cheatcodes. In this guide, we’ll cover 8 of these cheatcodes. To see all of the available cheatcodes, head to the docs here.

Initial setup

Like before, open your terminal and create a new Foundry project like so:

forge init cheatcodes-foundry

Delete script/Counter.s.sol as we don’t need it

Grab a public RPC URL for Ethereum Mainnet from here. Create a .env file in the root of your project and paste the URL like so.

 MAINNET_RPC_URL=https://eth.merkle.io

Writing the contract

Let’s write a contract which will allow us to see Foundry’s cheatcodes in action. Rename Counter.sol in src to Cheatcodes.sol and replace the existing code with this snippet:

// SPDX-License-Identifier: MIT
pragma solidity ^0.8.13;

contract Cheatcodes {

    uint256 public value;
    address public dummyAddress;

    error InsufficientBalance();
    event ValueChanged (address indexed senderAddress); 


    function setValue(uint256 _value) public {
        value = _value;
        dummyAddress = msg.sender;

        emit ValueChanged(msg.sender);

    }


    function requirePayment() external payable {
        if (msg.value < 1 ether) revert InsufficientBalance();
        value = msg.value;
    }
}

The code here is fairly simple. We have declared 1 error and 1 event as we will be testing some cheatcodes that involve them. The requirePayment function reverts with the defined error if someone calls it with a value that is less than 1 ether. Let’s learn about some cheatcodes and their corresponding tests now

Cheatcodes and their tests

Rename Counter.t.sol to Cheatcodes.t.sol and paste the following boilerplate code there:

// SPDX-License-Identifier: UNLICENSED
pragma solidity ^0.8.13;

import {Test, console} from "forge-std/Test.sol";
import {Cheatcodes} from "../src/Cheatcodes.sol";

contract CheatcodesTest is Test {
    Cheatcodes public cheatcodeContract;
    event ValueChanged (address indexed senderAddress); 

    function setUp() public {
        cheatcodeContract = new Cheatcodes();
    }


}

Great now let’s move on to adding cheatcodes

  1. vm.addr: it computes the address for a given private key, where the private key is the parameter of the function. If you give it an integer like 1, it gets converted to hexadecimal and padded to reach the fixed length of 32 bytes, so we’re actually supplying 0x0000000000000000000000000000000000000000000000000000000000000001 as the private key. Just add this line of code in your test, right after the ValueChanged event:

address someAddress = vm.addr(1);
  1. vm.prank: sets msg.sender to the specified address for the next call.

  2. vm.expectEmit: it asserts that a specific log is emitted during the next call. We first call the cheatcode, then emit the event we want to see being emitted by the next contract call, and then make that contract call in question.

Let’s see vm.prank and vm.expectEmit together:

function test_prankAndExpectEmit() public {
        vm.prank(someAddress);
        vm.expectEmit();
        emit ValueChanged(someAddress);
        cheatcodeContract.setValue(23);
        assertEq(address(cheatcodeContract.dummyAddress()), someAddress);

    }

In this function, we impersonate the address held by the someAddress variable, so the transaction is sent with that address as the msg.sender. Then, we use vm.expectEmit, emit the event we want to see being emitted by the setValue function of our contract and then call the setValue function. Finally, we assert that the value of dummyAddress in the contract changed to someAddress.

  1. vm.deal: It allows us to arbitrarily change the balance of some address. Add this function to your tests
function test_deal() public{
        vm.deal(someAddress, 1 ether);
        assertEq(someAddress.balance, 1 ether);
    }

In this test, we just give 1 ether to someAddress and assert that their balance did indeed get changed to 1 ether

  1. vm.expectRevert: it asserts that the next contract call should revert. Add this function to your tests
function test_revert() public{
        vm.prank(someAddress);
        vm.expectRevert();
        cheatcodeContract.requirePayment{value: 1 ether}();
    }

Remember that the setup function runs before every test case, which in our case means that a new instance of the Cheatcodes contract is created before every test. So in test_revert, the balance of someAddress is 0 since it isn’t affected by what we did in test_deal. We call our contract’s requirePayment function via someAddress, which doesn’t have balance so the call will revert. vm.expectRevert asserts this too and our test will thus pass if the call reverts. Ironic.

  1. hoax: I may have hidden something from you lol. You don’t need to do prank + deal to impersonate some address which has balance. You can just use hoax instead. Add this code to your tests
    function test_hoax() public{
        hoax(someAddress, 2 ether);
        cheatcodeContract.requirePayment{value: 1 ether}();
        assertEq(address(cheatcodeContract).balance, 1 ether);
    }

In this snippet, we use hoax to both give some ether to someAddress and impersonate it too. We then call our contract’s requirePayment function with 1 ether and finally assert that the balance of our contract should be 1 ether after the function runs.

  1. vm.envString: for reading environment variables written in the .env file

  2. vm.createSelectFork: it creates and selects a new fork from the given endpoint and returns the identifier of the fork.

Now, let’s see vm.envString and vm.createSelectFork together

function test_createSelectForkAndEnvString() public {
        vm.createSelectFork(vm.envString("MAINNET_RPC_URL"));
        address benDotEth = 0x91364516D3CAD16E1666261dbdbb39c881Dbe9eE;
        assertTrue(benDotEth.balance>0);
    }

In this test, we use our Mainnet RPC URL to fork Ethereum Mainnet and check the balance of ben.eth. We then assert that this balance is greater than 0.

If this test fails, make sure you’ve written everything correctly and if it still fails, it just means that ben.eth's wallet got drained. Too bad.

Running the tests

The final code of your Cheatcodes.t.sol file should look something like this:

// SPDX-License-Identifier: UNLICENSED
pragma solidity ^0.8.13;

import {Test, console} from "forge-std/Test.sol";
import {Cheatcodes} from "../src/Cheatcodes.sol";

contract CheatcodesTest is Test {
    Cheatcodes public cheatcodeContract;
    event ValueChanged (address indexed senderAddress); 


    address someAddress = vm.addr(1);

    function setUp() public {
        cheatcodeContract = new Cheatcodes();
    }


    function test_prankAndExpectEmit() public {
        vm.prank(someAddress);
        vm.expectEmit();
        emit ValueChanged(someAddress);
        cheatcodeContract.setValue(23);
        assertEq(address(cheatcodeContract.dummyAddress()), someAddress);

    }

    function test_deal() public{
        vm.deal(someAddress, 1 ether);
        assertEq(someAddress.balance, 1 ether);
    }

    function test_revert() public{
        vm.prank(someAddress);
        vm.expectRevert();
        cheatcodeContract.requirePayment{value: 1 ether}();
    }

    function test_hoax() public{
        hoax(someAddress, 2 ether);
        cheatcodeContract.requirePayment{value: 1 ether}();
        assertEq(address(cheatcodeContract).balance, 1 ether);
    }

    function test_createSelectForkAndEnvString() public{
        vm.createSelectFork(vm.envString("MAINNET_RPC_URL"));
        address benDotEth = 0x91364516D3CAD16E1666261dbdbb39c881Dbe9eE;
        assertTrue(benDotEth.balance>0);
    }

}

To run these tests, just execute this command in your terminal

forge test

You should see something like this:

Recall that you can adjust the verbosity for seeing logs in more detail.

Conclusion

Congratulations, you have successfully learned how you can use cheatcodes in Foundry! And another massive congratulations for finishing Foundry Mode! What now? Well, you get to decide that! Go over some of the articles again if you feel like you need to revise, play around with the code you’ve written, find the next idea that will “onboard one billion people to crypto”, or take a break and figure out what you truly want to do. Thanks a lot for reading this article to the end. Thanks a lot for reading Foundry Mode to the end. I will see you again, very soon 🫡🫡