Back to glossary

Invariant Test

Table of Contents

What is an invariant test?

An invariant test is a code-analysis process that seeks to verify certain predefined properties or rules, called invariants, remain true throughout all possible states and executions of a smart contract.

An invariant test requires:

  • Invariant definition: A condition that must hold true regardless of the input to the function.
  • State analysis: Testing a smart contract in a number of different states and with a variety of input values. The goal is to confirm each defined invariant does not break under all testing scenarios.

For instance, in an ERC-20 token smart contract, an invariant test could verify that the total supply of tokens decreases by the same amount as the number of tokens burned. If the test finds a violation, it indicates a flaw in the smart contract's design that must be addressed.

function invariant_burnEffect(address burner, uint256 amountToBurn) public {
   
    require(token.balanceOf(burner) >= amountToBurn, "Insufficient balance");

    uint256 initialTotalSupply = token.totalSupply();

    // The burn operation
    token.burn(burner, amountToBurn);

    uint256 finalTotalSupply = token.totalSupply();

    assert(finalTotalSupply == initialTotalSupply - amountToBurn);

    assert(finalTotalSupply + amountToBurn == initialTotalSupply);
}

How does invariant testing work?

Invariant tests are performed through a process called fuzz testing. Once key invariants are defined, fuzzers generate random or semi-random inputs to simulate different scenarios and evaluate a smart contract’s behavior during execution.

Fuzzers carry out two types of tests: stateful testing and stateless testing.

Stateless Fuzz Testing

Stateless fuzz testing seeks to detect issues caused by specific function calls or inputs while ignoring. Each test runs independently, with inputs chosen at random. Before each new test run, the contract's state is reset, so all changes made in previous tests are lost. 

Stateful Fuzz Testing

Stateful fuzz testing extends the concept of stateless fuzz testing by preserving the contract's state across test runs. The test checks whether the defined invariants are valid throughout the entire sequence of operations.

This involves executing a series of function calls in random order with various inputs, where each call potentially modifies the state. 

For a deeper understanding of how invariant testing works, including examples, read stateless and stateful fuzzing in Foundry.

How to define invariants for an invariant test

In addition to a smart contract’s business goal and logic, defining an invariant requires a thorough understanding of all variables, functions, and events that make up the smart contract. Taken together, these determine what must remain constant, what conditions must always be met, and what the contract needs to maintain at all times.

From there, follow three steps to create well-defined invariants:

1. Start with a clear, concise invariant definition

Articulate invariants in simple terms with straightforward language. They should precisely describe a condition that must always hold true within the system’s operation. 

For example, an invariant for a lending contract could be: A user's borrowed amount shall never exceed their collateral value.

2. Translate invariants into code 

Once defined, convert invariants into Solidity code or another language. The translation must preserve the original intent to guarantee the smart contract behaves as expected.

In Solidity, assertions and other control mechanisms (such as require() and assert()) are used to specify test parameters to run against defined invariants.
For example : assert(borrowedAmount <= collateralValue);

3. Start small and iterate for complexity

Keep things manageable and focus on the most important properties first to ensure their soundness. Once core invariants are validated, gradually expand the testing suite to include more complex and edge case invariants.

Tools for invariant tests

Invariant testing tools generate test inputs using various techniques to reveal unexpected behavior, such as crashes, incorrect logic, or security vulnerabilities. Inputs include:

  • Normal, commonly used data a program is designed to handle. 
  • Data at the edge of acceptable ranges, like maximum or minimum allowable values.
  • Unexpected, out-of-range, or nonsensical inputs, such as negative values where only positive ones are allowed.

Tools like Foundry, Medusa, and Echidna help developers perform invariant tests. These tools support both stateless and stateful fuzz testing.

Foundry's invariant tests are always stateful fuzz tests. The following is an example of a stateful text using Foundry. To learn more about invariant tests with Foundry, read: Smart Contract Fuzzing and Invariants Testing with Foundry.

Benefits of invariant testing

An invariant test improves system security by systematically identifying vulnerabilities, false assumptions, and incorrect logic across the entire spectrum of possible states and scenarios that unit tests or manual code reviews may miss. Validating invariants benefits the system by:

  1. Identifying bugs and logic errors in the development process, reducing the likelihood that vulnerabilities will be deployed into production.
  2. Ensuring smart contracts perform as designed under a wide range of conditions.
  3. Increasing the security of the system.

Learn how to incorporate invariant testing in your development process on Cyfrin Updraft.

Related Terms

No items found.