Back to blogs
Written by
Vasiliy Gualoto
Published on
March 3, 2024

Smart Contract Fuzz Tests Using Foundry | Full guide (updated)

Learn how to write Solidity smart contract fuzz tests (fuzzing) using the Foundry framework. Write tests, use prank addresses, and execute them using forge.

Table of Contents

This article teaches how to write Solidity smart contrac fuzz tests (fuzzing) to help your writing more secure protocols and uncover issues in your codeo.

Smart contracts fuzzing is the new floor for smart contract security and a must for any developer before deploying code to a blockchain. Plenty of tools are available on the market to perform fuzzing, and in today's blog, we will delve into Fuzzing with Foundry.

For those who don’t know what Solidity smart contract Fuzzing Invariant testing is, don’t forget to check out our specialized article explaining this technique's nuances, with simple examples and analogies.

If you haven’t ever coded a single line of code, check out our Ultimate Blockchain Developer course from zero to expert on Updraft.

Before getting started with smart contract fuzzing, let's quickly understand what an invariant is, as it's a key component to keep in mind when setting up our fuzz tests.

Defining invariant in your Solidity smart contract

An invariant is a condition that the system must always hold, regardless of the contract’s state or input.

In DeFi, a good invariant might be:

  • The number of votes cast must not exceed the number of registered voters.
  • A user should never be able to withdraw more money than they deposited.
  • There can only be one winner of the fair lottery.

Foundry defines an invariant test as a stateful fuzz test. Still, this definition is not entirely accurate as we can perform any test to an invariant, as seen in the following sections.

Stateless Solidity smart contract fuzz testing

To perform a stateless fuzz test on your Solidity smart contract using Foundry, on which the state of the variables will be forgotten on each run, let’s consider a simple example - If you want to follow along with the code, you can start a new Foundry project.

forge init 

Now, let’s create a smart contract  called SimpleDapp that will allow users to deposit and withdraw funds from the protocol and the contract looks as follows:

//SPDX-License-Identifier: MIT

pragma solidity ^0.8.23;

/// @title SimpleDapp
/// @notice This contract allows for deposits and withdrawals of ETH by users
contract SimpleDapp {
    mapping(address => uint256) public balances;

    /// @notice Deposit ETH into the contract
    /// @dev This function will deposit ETH into the contract and update the mapping balances.abi
    function deposit() external payable {
        balances[msg.sender] += msg.value;
    }

    /// @notice Withdraw ETH from the contract
    /// @dev This function will withdraw ETH from the contract and update the mapping balances.
    /// @param _amount The amount of ETH to withdraw
    function withdraw(uint256 _amount) external {
        require(balances[msg.sender] >= _amount, "Insufficient balance");
        balances[msg.sender] -= _amount;
        (bool success, ) = msg.sender.call{value: _amount}("");
        require(success, "Withdraw failed");
    }
}

The invariant, or the property that the system must always hold for this contract, is: a user should never be able to withdraw more money than they deposited.

Setting up a Stateless fuzz test for this Solidity contract is easy in Foundry, and we need to create a test as we always do on this framework.

First, we need to use the basic imports and define a setup function

//SPDX-License-Identifier: MIT

import {Test} from "forge-std/Test.sol";
import {SimpleDapp} from "../src/SimpleDapp.sol";

pragma solidity ^0.8.23;

/// @title Test for SimpleDapp Contract
/// @notice This contract implements FUzz testing for SimpleDapp
contract SimpleDappTest is Test {
    SimpleDapp simpleDapp;
    address public user;

    ///@notice Set up the test by deploying SimpleDapp
    function setUp() public {
        simpleDapp = new SimpleDapp();
        user = address(this);
    }

}

Then, we can easily set up a test for this contract. The key to making them a stateless fuzz test is that instead of using the test parameters burned on the code, we set them up as input parameters; this way, Foundry will automatically start throwing random input data at them.

/// @notice FUzz test for deposit and Withdraw functions
    /// @dev Test the invariant that a user can't withdraw more than they deposit
    /// @param depositAmount The amount of ETH to deposit
    /// @param withdrawAmount The amount of ETH to withdraw
    function testDepositAndWithdraw(
        // We set the depositAmount and withdrawAmount to be input parameters 👇👇👇
        uint256 depositAmount,
        uint256 withdrawAmount
    )
        public
        payable
    // Foundry will generate random values for the input parameters 👆👆👆
    {
        // Ensure the user has enough Ether to cover the deposit
        uint256 initialUserBalance = 100 ether;
        vm.deal(user, initialUserBalance);

        // Only attempt deposit if the user has enough balance
        if (depositAmount <= initialUserBalance) {
            simpleDapp.deposit{value: depositAmount}();

            if (withdrawAmount <= depositAmount) {
                simpleDapp.withdraw(withdrawAmount);
                assertEq(
                    simpleDapp.balances(user),
                    depositAmount - withdrawAmount,
                    "Balance after withdrawal should match expected value"
                );
            } else {
                // Expect a revert due to insufficient balance
                vm.expectRevert("Insufficient balance");
                simpleDapp.withdraw(withdrawAmount);
            }
        }
    }

For this test, the fuzzer will try random values for both variables depositAmount and withdrawAmount. If the withdrawal amount exceeds the deposited amount, the test will fail; let’s try it using the command:

test --mt testDepositAndWithdraw -vvv

As expected, this will throw us an error stating that the invariant condition was violated as all the deposits and withdrawals are random; there will be a scenario in which the withdrawal value will be greater than the deposited one.

The image shows the results of a stateless fuzz test using foundry

As you can see, together with the number used to break our function, there’s another parameter: “runs” - this represents the number of randomly generated inputs the fuzzer went through before it found the CounterExample. If the fuzzer tests thousands of potential counterexamples and none of them work because there isn’t a bug, then you might end up waiting eternally.

To solve this, we can set up the maximum amount of runs the fuzzer will try before stopping; in Foundry, we need to access the configuration file foundry.toml.

The image shows the folder structure of the foundry dot toml file to configure tests

Then, we can set up a new parameter called [fuzz] and manually state the maximum amount of runs. The end result will look something like this.

[profile.default]
src = "src"
out = "out"
libs = ["lib"]

# See more config options https://github.com/foundry-rs/foundry/blob/master/crates/config/README.md#all-options

[fuzz]
runs = 1000

Stateful Solidity smart contract fuzz testing

For this type of test, the state of the variables is remembered across multiple runs, and we need to go through some unique configurations on Foundry to make this work.

Let’s explore a different example for a new contract called AlwaysEven.sol; this time, we have set up an invariant for a variable called alwaysEvenNumber, and the condition that it must always hold is that the variable must always be even, never odd.

So, the contract is as follows.

// SPDX-License-Identifier: MIT

pragma solidity ^0.8.12;

contract AlwaysEven {
    uint256 public alwaysEvenNumber;
    uint256 public hiddenValue;

    function setEvenNumber(uint256 inputNumber) public {
        if (inputNumber % 2 == 0) {
            alwaysEvenNumber += inputNumber;
        }

        // This conditional will break the invariant which must be always be even
        
        if (hiddenValue == 8) {
            alwaysEvenNumber = 3;
        }
        // We set the hiddenValue to the inputNumber at the end of the function
        // In a stateful scenario, this value will be remembered for the next call

        hiddenValue = inputNumber;
    }
}

We also include another variable called hiddenValue, which can change the value of alwaysEvenNumber to the odd value of three. As the state of the variables will be remembered, this will most likely break the invariant condition.

Setting up the Stateful Test

First, we need an extra import from the standard forge-std library as follows:

import {StdInvariant} from "forge-std/StdInvariant.sol"; 

We need to add this as part of the inheritance of our test contract like this:

contract AlwaysEvenTestStateful is StdInvariant, Test {}

From the StdInvariant.sol we get a new function called targetContract. This will allow us to define the contract we will put to the test.

The exciting part of this is that by defining a target contract, Foundry will automatically start executing all the contract functions randomly and setting random input parameters as well. To define the target contract, we need to set it up on the setup function.

function setUp() public {
        targetContract(address(SelectedContract));
    }

Finally, we can set up the test. This time, we don’t need to include an input parameter for the test, and as the function will be executed automatically, we need the assertion statement. The final result would look like this:

// SPDX-License-Identifier: MIT

import {Test} from "forge-std/Test.sol";
import {AlwaysEven} from "../src/AlwaysEven.sol";

// We need to import the invariant contract from forge-std
import {StdInvariant} from "forge-std/StdInvariant.sol";

pragma solidity ^0.8.12;

contract AlwaysEvenTestStateful is StdInvariant, Test {
    AlwaysEven alwaysEven;

    function setUp() public {
        alwaysEven = new AlwaysEven();
        targetContract(address(alwaysEven));
    }

    function invariant_testsetEvenNumber() public view {
        assert(alwaysEven.alwaysEvenNumber() % 2 == 0);
    }
}

When the fuzzer starts throwing random data to the functions, it will eventually set an input parameter of 8, ultimately making the invariant condition break and resulting in an error. Let’s run the test using the command:

forge test --mt invariant_testsetEvenNumber -vvv

We will have a result like this one.

The image shows the results of a stateful solidity smart contract fuzz test using foundry

A note on fuzz testing terminology for Solidity smart contracts

When it comes to different types of tests, terminology can be confusing.  Foundry often categorizes an invariant test as a stateful fuzz test, even though we can perform any test using an invariant, from unit tests to any fuzz test.

To clarify these distinctions, here's a detailed graph by Nisedo outlining the various test types.

The image shows different types of solidity smart contract testing

So, remember that you can define an invariant - or property that the system must always hold and perform any test you want to it. Foundry requires you to use the keyword invariant for stateful invariant fuzz testing, but that does not mean that it is the only type of invariant testing.

Conclusion

Adopting fuzz and invariant testing transcends standard practice in smart contract development—it's a vital necessity.

We hope you enjoyed this guide to performing Solidity smart contract invariant fuzz testing using the Foundry framework. Plenty of tools exist, but Foundry stands on the top as it allows quick development of smart contracts.

If you want to tinker with this code, don’t forget to check out the contract source code on GitHub.

Secure your protocol today

Join some of the biggest protocols and companies in creating a better internet. Our security researchers will help you throughout the whole process.
Stay on the bleeding edge of security
Carefully crafted, short smart contract security tips and news freshly delivered every week.