Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat: Add invariant testing #760

Merged

Conversation

lucas-manuel
Copy link
Contributor

@lucas-manuel lucas-manuel commented Jan 6, 2023

closes #497

@@ -0,0 +1,3 @@
{
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

remove

Copy link
Contributor

@mds1 mds1 left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Love it so far, just took a pass and left some small comments. Think I'll need to see it a bit more fleshed out before coming up with some naming ideas

projects/invariant_testing/Basic4626Deposit.sol Outdated Show resolved Hide resolved
projects/invariant_testing/Basic4626Deposit.sol Outdated Show resolved Hide resolved
projects/invariant_testing/Basic4626Deposit.sol Outdated Show resolved Hide resolved
src/forge/advanced-testing.md Outdated Show resolved Hide resolved
src/forge/invariant-testing.md Outdated Show resolved Hide resolved
src/forge/invariant-testing.md Outdated Show resolved Hide resolved
src/forge/invariant-testing.md Outdated Show resolved Hide resolved
src/forge/invariant-testing.md Outdated Show resolved Hide resolved
@PaulRBerg
Copy link
Contributor

PaulRBerg commented Jan 23, 2023

I also read through the first draft of this, and I like it a lot - it's straight to the point.

My documentation requests:

  1. The fact that Forge considers all functions that start with invariant_ an invariant test - I think that this should be said at the beginning of the document.
  2. Explain what the call_override config option does.
  3. Explain how targetSender and excludeSender work.

@PaulRBerg
Copy link
Contributor

PaulRBerg commented Jan 24, 2023

@lucas-manuel, quick terminology question.

In August 2022, you referred in this comment to the target contracts that encapsulate the calls to the real contracts that are to be invariant tested as "actors".

However, in this PR, it looks like you have changed the name to "handler" - am I correct that you now prefer this terminology over "actor"?

@lucas-manuel
Copy link
Contributor Author

@lucas-manuel, quick terminology question.

In August 2022, you referred in this comment to the target contracts that encapsulate the calls to the real contracts that are to be invariant tested as "actors".

However, in this PR, it looks like you have changed the name to "handler" - am I correct that you now prefer this terminology over "actor"?

Thats right - we're moving to handlers since the term "actor" was used when we had to use one contract per user, before pranking was possible. Now that multiple actors can be used within a give contract, we determined that handler should be used instead.

@PaulRBerg
Copy link
Contributor

PaulRBerg commented Jan 25, 2023

Thanks for confirming, @lucas-manuel.

Speaking of pranking multiple actors - the useRandomLp modifier used in maple-core-v2 is something that all invariant users may need. We should consider renaming it to something more general-purpose (e.g. useRandomUser or useRandomActor) and ship it with the InvariantTest contract part of Forge Std.

@PaulRBerg
Copy link
Contributor

Potentially dumb question - can vm.assume be used in invariant tests?

I see no reference to this cheatcode in Lucas' examples, but I'm not sure if this is because Maple uses a different constraining system (constrictToRange), or because fuzzing assumptions are not meant to be used in an invariant context. And if not, why? It might be worth it to explain this in the docs.

If I understand this correctly, under "Bounded Testing", the user is expected to bound the inputs to avoid reverts.

@lucas-manuel
Copy link
Contributor Author

Gonna work to push this through now, what is missing? @mds1 @gakonst @PaulRBerg @pcaversaccio

@lucas-manuel
Copy link
Contributor Author

Potentially dumb question - can vm.assume be used in invariant tests?

I see no reference to this cheatcode in Lucas' examples, but I'm not sure if this is because Maple uses a different constraining system (constrictToRange), or because fuzzing assumptions are not meant to be used in an invariant context. And if not, why? It might be worth it to explain this in the docs.

If I understand this correctly, under "Bounded Testing", the user is expected to bound the inputs to avoid reverts.

Correct me if I'm wrong @mds1 but I believe vm.assume would work in the same way as in fuzz tests for this. Haven't used it personally though.

@lucas-manuel
Copy link
Contributor Author

Thanks for confirming, @lucas-manuel.

Speaking of pranking multiple actors - the useRandomLp modifier used in maple-core-v2 is something that all invariant users may need. We should consider renaming it to something more general-purpose (e.g. useRandomUser or useRandomActor) and ship it with the InvariantTest contract part of Forge Std.

This is an interesting idea, thoughts @mds1 ?

@pcaversaccio
Copy link
Contributor

pcaversaccio commented Jan 26, 2023

Gonna work to push this through now, what is missing? @mds1 @gakonst @PaulRBerg @pcaversaccio

Proper descriptions on all the invariant helper functions like targetSender, targetArtifact etc. would be very nice.

@PaulRBerg
Copy link
Contributor

PaulRBerg commented Jan 26, 2023

I believe vm.assume would work in the same way as in fuzz tests for this

It doesn't. I'm getting Failed Assertion errors if I have vm.assume statements in the invariant tests. If this is not intended behavior, should I open an issue in forge-std?

Proper descriptions on all the invariant helper functions

Yes! A markdown table for this would be awesome.

Gonna work to push this through now, what is missing?

The greatest conceptual leap for me were the handler contracts - I spent a few days looking at the maple-core-v2 repository, and the LoanHandler and the LpHandler contracts were the most difficult to understand.

I see that you added more explanations and a few diagrams about them in the meantime (6 hrs ago), which are awesome. But I think that there are still a few other core concepts that would be good to document, too:

  1. How should actors be generated? This may depend upon the business logic of the smart contract under test, but we should be able to come up with a few rules of thumb, e.g. generate the actors in the constructor and store them in an array.
  2. How should actors and other contract storage items be fuzzed? In maple-core-v2, you are using these seed arguments (warpSeed_, borrowerIndexSeed_), and it took me a while to understand their role. It would also be good to mention the need for a useRandomActor modifier - at least until Foundry offers a built-in solution for this.
  3. Should handler contracts inherit from Test? Or Utils? If not, how can they get access to vm, bound, and so forth?

@mds1
Copy link
Contributor

mds1 commented Jan 26, 2023

It doesn't. I'm getting Failed Assertion errors if I have vm.assume statements in the invariant tests. If this is not intended behavior, should I open an issue in forge-std?

@PaulRBerg Nice catch, just created foundry-rs/foundry#4190 to track

Speaking of pranking multiple actors - the useRandomLp modifier used in maple-core-v2 is something that all invariant users may need. We should consider renaming it to something more general-purpose (e.g. useRandomUser or useRandomActor) and ship it with the InvariantTest contract part of Forge Std.

This is an interesting idea, thoughts @mds1 ?

This is an interesting idea. More generally maybe we can add a template InvariantHandler contract to forge-std. @lucas-manuel @PaulRBerg if either of you have ideas on what this could look like, create an issue in forge-std and we can continue the conversation there.

Gonna work to push this through now, what is missing? @mds1 @gakonst @PaulRBerg @pcaversaccio

Awesome, thanks! Later today I'll review what's currently here and reply with some thoughts

@PaulRBerg
Copy link
Contributor

More generally maybe we can add a template InvariantHandler contract to forge-std.

I like this idea. Definitely one or two helpers for actor generation and management would be nice. I may open an issue for this later.

@gakonst
Copy link
Member

gakonst commented Feb 5, 2023

@lucas-manuel I think one final pass based on the above comments and we're good to merge as a first iteration

@pcaversaccio
Copy link
Contributor

pcaversaccio commented Feb 6, 2023

Haven't checked again the content of this PR but apparently you thought it would be a good idea to rename InvariantTest to StdInvariant (foundry-rs/forge-std@b4f1215). Broke all of my invariant tests in the CI lol - in any case if we somewhere use the old term InvariantTest this should be updated.

@PaulRBerg
Copy link
Contributor

PaulRBerg commented Feb 6, 2023

@pcaversaccio you should set the branch to v1 in your .gitmodules (foundry-rs/forge-std#275).

Nonetheless, I agree with you that all references to InvariantTest should be changd to StdInvariant.

@pcaversaccio
Copy link
Contributor

pcaversaccio commented Feb 6, 2023

@PaulRBerg yeah agreed - however in my particular case here I didn't want to use the v1 branch since the development iterations where important to me (i.e. invariant testing) and therefore I always pulled directly the master branch every two days or so. That's why I immediately realised that something is broken. But generally the pinning or release-branch strategy you're suggesting is also something I usually follow.

@pcaversaccio
Copy link
Contributor

pcaversaccio commented Feb 6, 2023

Also, I quickly wanted to emphasise that people should know that Test inherits from it by default now. Because if you do the following MyContract is Test, StdInvariant {...} it will not compile due to the linearisation pattern by Solidity. However, folks will read the release of forge-std and will get confused:
image

@lucas-manuel
Copy link
Contributor Author

Speaking of useful modifiers, something like this may also be worth documenting:

mapping(string => uint256) public calls;
uint256 public totalCalls;

modifier instrument(string memory func) {
    calls[func]++;
    totalCalls++;
    _;
}

This can be applied to handler contract functions like this:

function myFunc() external instrument("myFunc") {
    // ...
}

The purpose being to get a call summary (e.g. here).

However, I am not sure whether adding a note about the above is worth it given the intention to add native support for instrumentation in Foundry (foundry-rs/foundry#3607).

I like this idea but yeah I think we should push for the native support, thoughts @mds1 @gakonst ?

@mds1
Copy link
Contributor

mds1 commented Feb 6, 2023

However, folks will read the release of forge-std and will get confused

The screenshot is correct when using v1.3.0 of forge-std, when v1.4.0 is released the release notes will document the breaking change and new usage info.

I agree with updating the docs to reflect this upcoming usage / naming though, cc @lucas-manuel

I like this idea but yeah I think we should push for the native support, thoughts @mds1 @gakonst ?

Agreed native support is preferable. There may be higher priority fixes/features though so I do think a template handler contract / modifiers in forge-std would be useful in the meantime. I also like this pattern that I recently came across from @horsefacts, and people can then implement their own getters to parse the calldata

@lucas-manuel
Copy link
Contributor Author

However, folks will read the release of forge-std and will get confused

The screenshot is correct when using v1.3.0 of forge-std, when v1.4.0 is released the release notes will document the breaking change and new usage info.

I agree with updating the docs to reflect this upcoming usage / naming though, cc @lucas-manuel

I like this idea but yeah I think we should push for the native support, thoughts @mds1 @gakonst ?

Agreed native support is preferable. There may be higher priority fixes/features though so I do think a template handler contract / modifiers in forge-std would be useful in the meantime. I also like this pattern that I recently came across from @horsefacts, and people can then implement their own getters to parse the calldata

Should I add these to the docs?

@gakonst
Copy link
Member

gakonst commented Feb 6, 2023

I've talked about Native support here: foundry-rs/foundry#3607 (comment). We could but I think is for separate discussion.

horsefact's pattern is still not too common so I'd avoid including it

anything else we need to get this over the line?

@lucas-manuel
Copy link
Contributor Author

Just adding the target definitions and will push for final review.

@lucas-manuel lucas-manuel changed the title feat: Add invariant testing [WIP] feat: Add invariant testing Feb 6, 2023
@lucas-manuel
Copy link
Contributor Author

Alright good for review 👍

@horsefacts
Copy link

horsefact's pattern is still not too common so I'd avoid including it

Agree, this is more an exploration than something I'd recommend right now.

There are different ways to assert invariants:
Examples of invariants are:
- *"The xy=k formula always holds"* for Uniswap
- *"The sum of all owner balances is equal to the total supply"* for an ERC-20 token.
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Nit, since "owner" sounds like a privileged actor

Suggested change
- *"The sum of all owner balances is equal to the total supply"* for an ERC-20 token.
- *"The sum of all user balances is equal to the total supply"* for an ERC-20 token.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Updated

@mds1
Copy link
Contributor

mds1 commented Feb 6, 2023

One comment here we should fix, but otherwise this looks really great! Awesome job @lucas-manuel 🔥

@lucas-manuel
Copy link
Contributor Author

One comment here we should fix, but otherwise this looks really great! Awesome job @lucas-manuel 🔥

Updated! 👍

@mds1
Copy link
Contributor

mds1 commented Feb 8, 2023

@gakonst @lucas-manuel anything blocking merge here?

think we might just need info around how targetContracts can change since it automatically adds contracts deployed during an invariant run, cc @joshieDo for details

@lucas-manuel
Copy link
Contributor Author

@gakonst @lucas-manuel anything blocking merge here?

think we might just need info around how targetContracts can change since it automatically adds contracts deployed during an invariant run, cc @joshieDo for details

Good to go on my end

Copy link
Contributor

@PaulRBerg PaulRBerg left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Just had a final read, looks lit. Thanks again @lucas-manuel for writing this up. There are only a few minor typos - besides these, LGTM.

I'm glad to see the useActor modifier included in this PR 🙌

Comment on lines 147 to 149
| `excludeSender(address newExcludedSender_)` | Adds a given address to the to the `_excludedSenders` array. This set of addresses is explictly excluded from the target senders. |
| `excludeArtifact(string memory newExcludedArtifact_)` | Adds a given string to the to the `_excludedArtifacts` array. This set of strings is explictly excluded from the target artifacts. |
| `targetArtifact(string memory newTargetedArtifact_)` | Adds a given string to the to the `_targetedArtifacts` array. This set of strings is used for the target artifacts. |
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Double to the to the typo.

### Target Contract Setup

Target contracts can be set up using the following three methods:
1. Contracts that are manually added to the the `targetContracts` array are added to the set of target contracts.
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Double the the typo.

Copy link
Contributor

@pcaversaccio pcaversaccio left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Fixed typo: explictly ==> explicitly & I think : makes more sense where I suggested it. Anything else LGTM.

image


Invariant testing is a powerful tool to expose incorrect logic in protocols. Due to the fact that function call sequences are randomized and have fuzzed inputs, invariant testing can expose false assumptions and incorrect logic in edge cases and highly complex protocol states.

Invariant testing campaigns have two dimensions, `runs` and `depth`.
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
Invariant testing campaigns have two dimensions, `runs` and `depth`.
Invariant testing campaigns have two dimensions, `runs` and `depth`:

| Function | Description |
|-|-|
| `excludeContract(address newExcludedContract_)` | Adds a given address to the `_excludedContracts` array. This set of contracts is explicitly excluded from the target contracts.|
| `excludeSender(address newExcludedSender_)` | Adds a given address to the to the `_excludedSenders` array. This set of addresses is explictly excluded from the target senders. |
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
| `excludeSender(address newExcludedSender_)` | Adds a given address to the to the `_excludedSenders` array. This set of addresses is explictly excluded from the target senders. |
| `excludeSender(address newExcludedSender_)` | Adds a given address to the to the `_excludedSenders` array. This set of addresses is explicitly excluded from the target senders. |

|-|-|
| `excludeContract(address newExcludedContract_)` | Adds a given address to the `_excludedContracts` array. This set of contracts is explicitly excluded from the target contracts.|
| `excludeSender(address newExcludedSender_)` | Adds a given address to the to the `_excludedSenders` array. This set of addresses is explictly excluded from the target senders. |
| `excludeArtifact(string memory newExcludedArtifact_)` | Adds a given string to the to the `_excludedArtifacts` array. This set of strings is explictly excluded from the target artifacts. |
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
| `excludeArtifact(string memory newExcludedArtifact_)` | Adds a given string to the to the `_excludedArtifacts` array. This set of strings is explictly excluded from the target artifacts. |
| `excludeArtifact(string memory newExcludedArtifact_)` | Adds a given string to the to the `_excludedArtifacts` array. This set of strings is explicitly excluded from the target artifacts. |

@lucas-manuel
Copy link
Contributor Author

Made all requested changes thanks @PaulRBerg @pcaversaccio

@gakonst
Copy link
Member

gakonst commented Feb 10, 2023

let's send it

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

Successfully merging this pull request may close these issues.

Document invariant testing + patterns
7 participants