On fuzzing, fuzz testing, stateless and stateful fuzzing, and then invariant testing and all those scary stuff
Published: 2024-07-02 07:23:42

For blockchain developers only

You might've heard the scary terms fuzzing, fuzz tests, invariant testing, stateless fuzzing etc. They're not as scary as they sound. But, what are they?

If you're a developer, you know about unit tests. What do they do? They isolate and test. What will happen if I call this function with this value, will it pass or fail? They're super important and handy in all parts of development. But, when you're building, say, a big DeFi app you really might want to add fuzz tests alongside with your unit tests. Why, and again man, what are they? Fuzzing, invariant testing and all?

People are mad. They do all sorts of crazy stuff. They're highly unpredictable. Especially when there's money, a substantial amount of money involved. So, it would be beneficial to assume that there will be people who'll try to break your application. Even if they don't try, your application might just crash in certain edge cases. You can't just try to think of every single situation that might occur in your colossal dApp, right? Now fuzzing comes to rescue.

Fuzz tests rush to your function(s) with hundreds, if not thousands of times with random data. Say that you have a function that takes a parameter and does an operation with it. In unit testing you'd give that parameter a value, you'd choose it. Fuzz tests give all sorts of random values to that parameter to try to break it. And if you're using Foundry, it is super easy to write them. You just write them like unit tests (with a couple of gotchas that I won't be talking about in this not-so-technical post) and instead of giving your parameter a value, you just leave it untouched. Foundry then knows that it should do fuzzing.

Okay, so fuzzing is when you basically attack your dApp with so many random cases to see if it'll break. Now, let's talk about invariant (stateful) and stateless fuzzing. They sound even scarier, but again, they are not.

Stateless fuzzing is easier to grasp. You have a function or functions, and you want to call them with random data, many times. All good.

But, what if you needed to call a function after a function precisely? Like, let's say you want to call a redeem function that your users can call when they want to take their money out, but if they don't yet have put any money (say, via a deposit function), how can they possibly call the redeem function? You most probably already wrote modifiers and checks for that situation. But, if you run stateless fuzz tests, Foundry does not know in what order it has to call what. At this point comes the stateful, or invariant testing method.

In order to understand this, we need to understand what do we meen by invariant. Invariant, is the thing that cannot change, a constant. In the case of a DeFi protocol, you know that the protocol always has to be over-collateralized, that is to say, it always needs to have more money than it lends. This is your invariant. We can never be under-collateralized, we always need more money than we gave in our system. 

Now, in invariant testing, since we've decided what our constant is (that the protocol always needs to be over-collateralized), we want to make sure that the user cannot withdraw any money before depositing them. We use a handler to define in what order we want to run our fuzz tests. In the example we've given, we want the script to first send money via deposit function and only afterwards call the redeem or withdraw function. 

Also we want to keep the state updated right? Because if we didn't update the state of the Dapp updated, i.e. if we didn't save the money deposited by the test user, when our script tries to call the withdraw or redeem function it would fail. So, we're trying to replicate the exact steps we want to test, with many many random values. That's invariant testing for you in simple terms.