Types of Invariants
As discussed in the testing overview chapter, invariants describe the "truths" of your system. These are unchanging properties that arise from the design of a codebase.
Note: We will interchange the use of the word property and invariant often. For all intents and purposes, they mean the same thing.
Defining and testing your invariants is critical to assessing the expected system behavior.
We like to break down invariants into two general categories: function-level invariants and system-level invariants. Note that there are other ways of defining and scoping invariants, but this distinction is generally sufficient to start fuzz testing even the most complex systems.
Function-level invariants
A function-level invariant can be defined as follows:
Definition: A function-level invariant is a property that arises from the execution of a specific function.
Let's take the following function from a smart contract:
function deposit() public payable {
// Make sure that the total deposited amount does not exceed the limit
uint256 amount = msg.value;
require(totalDeposited + amount <= MAX_DEPOSIT_AMOUNT);
// Update the user balance and total deposited
balances[msg.sender] += amount;
totalDeposited += amount;
emit Deposit(msg.sender, amount, totalDeposited);
}
The deposit
function has the following function-level invariants:
- The ETH balance of
msg.sender
must decrease byamount
. - The ETH of
address(this)
must increase byamount
. balances[msg.sender]
should increase byamount
.- The
totalDeposited
value should increase byamount
.
Note that there other properties that can also be tested for but the above should highlight what a function-level
invariant is. In general, function-level invariants can be identified by assessing what must be true before the execution
of a function and what must be true after the execution of that same function. In the next chapter, we will write a
fuzz test to test the deposit
function and how to use medusa to run that test.
Let's now look at system-level invariants.
System-level invariants
A system-level invariant can be defined as follows:
Definition: A system-level invariant is a property that holds true across the entire execution of a system
Thus, a system-level invariant is a lot more generalized than a function-level invariant. Here are two common examples of a function-level invariant:
- The
xy=k
constant product formula should always hold for Uniswap pools - No user's balance should ever exceed the total supply for an ERC20 token.
In the deposit
function above, we also see the presence of a system-level invariant:
The totalDeposited
amount should always be less than or equal to the MAX_DEPOSIT_AMOUNT
.
Since the totalDeposited
value can be affected by the presence of other functions in the system
(e.g. withdraw
or stake
), it is best tested at the system level instead of the function level. We will look at how
to write system-level invariants in the Writing System-Level Invariants chapter.