Skip to content

Commit c0ffeee

Browse files
feat: forge invariants
1 parent c0ffeed commit c0ffeee

2 files changed

Lines changed: 17 additions & 22 deletions

File tree

sites/wonderland/docs/testing/advanced-testing/invariants-writing.md

Lines changed: 7 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@
33
We take an opiniated approach of having targeted fuzzing campaign and formal verification. To maximize the efficiency (ie benefit/cost), we put a bigger emphasis on *what* we test than on *how* we do it - finding and formalizing the most important invariants being a key objective to do so.
44

55
We define *good* invariant as:
6+
67
- Informative: cover a behaviour not or only partially covered by existing tests (unit or integration) - they bring new information (for instance, an invariant testing if `require(sender == owner)` reverts when the sender is not the owner brings no new information compared to an unit test).
78
- Realistic: the pre-condition must be something which is expected or at least achievable without breaking, for instance, EVM invariants with cheatcodes.
89

@@ -11,6 +12,7 @@ To find such, we start by establishing a protocol accounting (assets, liabilitie
1112
## Establish protocol accounting
1213

1314
We’ll use the following example throughout this doc:
15+
1416
- WonderPump is a novel ICO protocol
1517
- It has a single contract, where Grifters can provide a type of tokens and users can send ETH to get it, following a pricing function.
1618
- The protocol is getting a fee on each sale, in ETH, which can be redeemed via a dedicated function.
@@ -33,8 +35,8 @@ And the related operations:
3335

3436
- A deposit from grifter should increase token deposit and token to sell (ACC-2)
3537
- A sale should: (ACC-3)
36-
- Lower token deposited and token to sell
37-
- Increase eth for grifter, for the protocol fee and the eth in balance
38+
- Lower token deposited and token to sell
39+
- Increase eth for grifter, for the protocol fee and the eth in balance
3840
- A fee withdrawn should lower eth protocol fee and eth in balance (ACC-4)
3941
- Grifter withdraw should lower eth for grifter and eth in balance (ACC-5)
4042

@@ -58,14 +60,14 @@ One noticeable exception are invariants around arithmetic (see infra).
5860
- Non-reversion invariants: after writing handler and properties around a call, it is often useful to add post-condition on the revert case (ie make sure every revert is explained)
5961
- Protocol shutdown: With the accounting invariants in place, one powerful way to test them is to have a shutdown mechanism, which will unwind the whole protocol (pausing, emptying balances, etc), making sure no funds are stuck and all parties are made whole.
6062

61-
### Arithmetic:
63+
### Arithmetic
6264

63-
Having tightly coupled invariant is, quite obviously, useless. One way to test complex arithmetic is to challenge its mathematical properties instead. These invariants should be reserved when the implementation is not straightforward (assembly or huge complexity).
65+
Having tightly coupled invariant is, quite obviously, useless (ie pasting the implementation in a test). One way to test complex arithmetic is to challenge its mathematical properties instead. These invariants should be reserved when the implementation is not straightforward (assembly or huge complexity).
6466

6567
example: x*y
6668

6769
- this should be commutative: x*y == y*x
6870
- associative: (x*y*)*z == x*(y*z)
6971
- 1 is the neutral element: if y = 1, x == k
7072
- 0 is the absorbing element: if y = 0, x == k == 0
71-
etc
73+
etc

sites/wonderland/docs/testing/advanced-testing/overview.md

Lines changed: 10 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -2,16 +2,16 @@
22

33
This part of the guide covers our best practices for more advanced testing techniques, which are done at the end of the development cycle only (as they're more time and caffeine-consuming) and test the system as a whole.
44

5-
We add more formalized testing suite following 2 different approaches:
5+
We add more formalized testing suite one or both of the following approaches:
66

77
- stateful property-based fuzzing using Forge
88
- stateless symbolic execution using Halmos or Kontrol
99

10-
Having these 2 extra-steps consolidates the testing framework built around our protocol: unit tests are studying how a given part is working, integration is ensuring how multiple parts are working together during defined cases, property-based fuzzing will now study how the whole protocol is working in a big number of cases with multiple interactions (stateful) and formal verification will study how the protocol is working in *every possible* case (usually statelessly).
10+
Having these extra-steps consolidates the testing framework built around our protocol: unit tests are studying how a given part is working, integration is ensuring how multiple parts are working together during defined cases, property-based fuzzing will now study how the whole protocol is working in a big number of cases with multiple interactions (stateful) and formal verification will study how the protocol is working in *every possible* case (usually statelessly). In practice, most of our invariants are tested via fuzzing.
1111

1212
# Properties & Invariants
1313

14-
During the design phase of a new project, we start collecting the protocol properties and invariants (either from the new design or pre-existing specification/requirements). At this stage of the development, this means having a bullet point list describing what and how the project will behave. It includes invariants, that are always true, or some more specific scenarios, called properties (the distinction being pedantic only, both terms are used interchangeably).
14+
During the design phase of a new project, we start collecting the protocol properties and invariants (either from the new design or pre-existing specification/requirements). At this stage of the development, this means having a bullet point list describing what and how the project will behave. It includes invariants, characteristics which are always true, or some more specific scenarios, called properties (the distinction being pedantic only, both terms are used interchangeably).
1515

1616
In short, think of how the protocol would be described as a whole, from a helicopter point of view. With the list of properties, someone should be able to create a system which will behave in the same way as the one tested, without knowing the implementation details.
1717

@@ -71,9 +71,9 @@ function mul(uint256 a, uint256 b) external returns(uint256) {
7171

7272
Many approaches to conduct a testing campaign can be taken and are “correct”, yet having a systematic and well-organized one helps *a lot*. Here are some take-aways from previous project, which *might* help:
7373

74-
- Start by reviewing the properties and invariants. See the doc above on how to find them, keeping in mind that **"having only a few selected critical (and well tested) invariants is therefore better than a list of 50 highly complex, tightly coupled to the implementation, ones"**.
74+
- Start by reviewing the properties and invariants. See the doc above on how to find them, keeping in mind that **"having only a few selected critical (and well tested) invariants is better than a list of 50 highly complex, poorly defined or understandable, ones"**.
7575
- Setup for fuzzing or formal verification can be taken from the integration tests - rather as a template than blindly pasting them, as to avoid inheriting any complexity debt coming from setting up a fork for instance.
76-
- We tend to start from one given path (ie "how to test the first invariant" for instance), including handlers to reach it, then incrementally add to cover other (instead of, for instance, adding handlers for every functions and deal with the complexity later).
76+
- We tend to start from one given path (ie "how to test the first invariant" for instance), including handlers to cover it, then incrementally add handlers to cover other paths (instead of, for instance, adding handlers for every functions and deal with the complexity later).
7777
- Test functions are easier to grasp when expressed in Hoare logic (precondition, including pranks; action, as a single call; postcondition). In the rare case where multiple actions are conducted within a single test function, this is expressed as multiple Hoare logic blocks.
7878

7979
```solidity
@@ -127,27 +127,20 @@ function testOne() public {
127127
128128
## Reporting found issues
129129
130-
Sometimes when testing, you will find bugs and hopefully they won’t be Medusa and Kontrol bugs but actual errors in the project’s code. The first step in handling the issue should always be notifying the dev team and validating the finding. Then depending on the validity and fixability of the issue we have multiple choices.
130+
Sometimes when testing, you will find bugs and hopefully they won’t be Forge and Kontrol bugs but actual errors in the project’s code. The first step in handling the issue should always be notifying the dev team and validating the finding. Then depending on the validity and fixability of the issue we have multiple choices.
131131
132132
### Invalid finding
133133
134134
If the finding could not be confirmed by the dev team, we should look for errors in the test and fix them.
135135
136136
### Valid finding
137137
138-
If the finding is confirmed, the first step is always gathering the details and filling in the report under the project’s internal testing page. During the process, you will assign the finding a unique ID.
138+
If the finding is confirmed, the first step is always gathering the details and filling in the report under the project’s internal testing page. During the process, you will assign the finding a unique ID and severity.
139139
140-
If the issue can be fixed, we should convert the Medusa / Kontrol test into a simple unit or integration test, and specify the issue ID in the test’s natspec.
140+
If the issue can be fixed, we should convert the Forge / Kontrol test into a simple unit or integration test, and specify the issue ID in the test’s natspec.
141141
142142
If the issue won’t be fixed, we still need a way to reproduce it. To simplify this process, we have settled on the following steps:
143143
144-
- If it doesn’t exist yet, create a `PropertiesFailing` contract and inherit it in `PropertiesParent`
144+
- If it doesn’t exist yet, create a `PropertiesFailing` contract and inherit it in `Invariant`
145145
- In the failing properties contract, add a test for the found issue. Make sure to specify the issue ID in the natspec.
146-
- Append the test name to the `excludeFunctionSignatures` property of the Medusa config. Note that the signature should start with the fuzz contract name:
147-
148-
```
149-
"excludeFunctionSignatures": [
150-
"FuzzTest.property_givenDepositsIsGreaterThanZero_totalWeights_isNotZero()"
151-
]
152-
```
153-
146+
- In the test setup, exclude this contract using `excludeContract(address(propertiesFailing))`

0 commit comments

Comments
 (0)