Fuzz Testing in Foundry

Fuzz Testing in Foundry - thirdweb Guides

In this guide, we will learn how to use fuzzing of inputs for property-based testing in Foundry. Property-based testing is a way to test general behaviours as opposed to isolated scenarios to check for bugs or unexpected behaviour.

What are Fuzzing and  Property-Based Testing?

Fuzzing or fuzz testing is a method of testing that involves providing the test, in this case, a property-based test, with large amounts of random input data, called "fuzz", in order to find bugs or vulnerabilities. The random input data generated by a fuzzer can be designed to exercise specific parts of a smart contract's code, such as error-handling routines, in ways that are difficult or impossible to achieve through manual testing.

Property-based testing is a technique for testing by specifying the general property that the contract should have and then generating random inputs to test whether it performs in accordance with that property. The goal of property-based testing is to find bugs in the contract by identifying inputs that cause it to violate the specified properties.

In Forge, fuzzing is used to quickly generate a large number of inputs, and then property-based testing can be used to specify behaviours the smart contract is expected to satisfy.

How to use Fuzzing in Foundry

Foundry has the added bonus over other frameworks that it has fuzzing inbuilt! Forge will run any test that takes at least one parameter as a property-based test.

To demonstrate the use of property-based testing and fuzzing in unit tests, let's use an ERC721 Drop prebuilt contract, with an external mint function, as an example contract to test:

contract NFTDrop is ERC721Drop {
    constructor(
        string memory _name,
        string memory _symbol,
        address _royaltyRecipient,
        uint128 _royaltyBps,
        address _primarySaleRecipient
    )
        ERC721Drop(
            _name,
            _symbol,
            _royaltyRecipient,
            _royaltyBps,
            _primarySaleRecipient
        )
    {}

    function mint(address _to, uint256 _amount) external {
        require(_amount > 0, "You must mint at least one token!");
        _safeMint(_to, _amount);
    }
}

To test the Mint function, we will want to test that the function does in fact mint the specified number of tokens to the address provided. We could write a unit test as follows:

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

import "forge-std/Test.sol";
import "../src/NFTDrop.sol";

contract NFTDropTest is Test {
    NFTDrop drop;
    address testAddr = makeAddr("Test");

    function setUp() public {
        drop = new NFTDrop("MintTest", "MT", testAddr, 500, testAddr);
    }

    function testMintWithZeroTokens() public {
        vm.expectRevert("You must mint at least one token!");
        drop.mint(testAddr, 0);
        assertEq(drop.balanceOf(testAddr), 0);
    }

    function testMintWithTwoTokens() public {
        drop.mint(testAddr, 2);
        assertEq(drop.balanceOf(testAddr), 2);
    }
}

To see a more in-depth explanation of this contract & testing in Foundry, check out this guide. To run this test, run forge test

This unit test does test that we can mint a token and that you have to mint more than one token. However, who is to say that it works for all amounts?

In order to write a property test, we need to determine a general property to test for. The general property to be tested here is: given a specified number of tokens, when minted, the address should receive that amount of tokens.

Given that Forge will treat any test that takes at least one parameter as a property-based test and fuzz the input parameter automatically, it is easy to rewrite the testMintWithTwoTokens test as follows :

function testMint(uint16 amount) public {
	vm.expectRevert("You must mint at least one token!");
	drop.mint(testAddr, amount);
	assertEq(drop.balanceOf(testAddr), amount);
}

We have restricted the input to uint16 because this corresponds to 2**16 or 65,536 tokens, which is a reasonable upper limit to test.

The test fails as it includes 0 as part of the fuzzed input
The test fails as it includes 0 as part of the fuzzed input

Oh no! This test will fail as it will include 0 as part of the fuzzed input. The assume cheatcode can be used to exclude certain cases, and in such instances, the fuzzer will ignore the inputs and begin a new fuzz run:

vm.assume(amount > 0);

Yay, our tests are now passing!

We can now deploy our contracts to Goerli testnet:

npx thirdweb deploy
💡
- "runs": the number of scenarios the fuzzer tests
- Default number of fuzz runs: 256 scenarios
- To change the number of fuzz runs, set the FOUNDRY_FUZZ_RUNS environment variable.

Thank You!

Now you know what property-based testing is and how to use fuzzing to further test your smart contracts using Foundry! It is important to rigorously test your smart contract's general behavior before moving from testnet to mainnet deployment so learning how to do so is a key part of the blockchain developer learning journey - congratulations!

To further your development journey, try property-based testing out in your own projects and share them with us in our Discord!