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.
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);
}
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 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 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.
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.
Invariant testing tools generate test inputs using various techniques to reveal unexpected behavior, such as crashes, incorrect logic, or security vulnerabilities. Inputs include:
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.
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:
Learn how to incorporate invariant testing in your development process on Cyfrin Updraft.