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
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 supplying0x0000000000000000000000000000000000000000000000000000000000000001
as the private key. Just add this line of code in your test, right after theValueChanged
event:
address someAddress = vm.addr(1);
vm.prank
: setsmsg.sender
to the specified address for the next call.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
.
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
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.
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 usehoax
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.
vm.envString
: for reading environment variables written in the.env
filevm.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 🫡🫡