-
Notifications
You must be signed in to change notification settings - Fork 11.9k
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
Add ERC4626 standard property tests #3792
Conversation
Hard to overstate how awesome this is. Thank you! We'll review the tests before merging. |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Run the tests, everything looks good.
Should we review in details what is in this submodule?
Awesome! Please let me know if you have any questions or concerns while reviewing the tests! BTW, fuzzing this new test currently takes ~3m on the github actions. Would it be problematic? |
@Amxx you're more than welcome to review them if you want!
|
99a220c
to
c1b96be
Compare
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
LGTM
Any reason why |
If I do
Is that a timeout? |
I'm glad you bring this up, which I wish I have a chance to discuss with you! Indeed, if you are lucky(?), you will get fuzz inputs that fail some "round-trip" property tests when this flag is set to true. For example, consider the following property, which says that when you withdraw some amount of assets and then immediately deposit the same amount again, the number of shares burned for the previous withdrawal must not be smaller than the number of shares minted for the deposit; because, otherwise, you can make a free profit by repeating withdrawal and deposit back and force. (Hence, called a round-trip property, and all the combinations are included in the tests.)
The main motivation behind this round-trip property is to check whether the standard-preferred rounding direction is employed by the vault implementations. Since your ERC4626 implementation follows the preferred rounding direction (which I appreciate!), all the round-trip property tests should pass...... as long as the vault is never emptied in the middle. For example, consider a vault where the current total assets is 20, and total supply (shares) is 10, and suppose that you hold all the shares. Now, suppose you withdraw 20 assets, and then deposit them back immediately (i.e., Now, the question is whether this behavior could be exploited or not. What concerns me is that, as you can see in the above example, there is a seemingly unintentional way to drastically decrease the share price (or increase depending on the context). Would it be too much to worry about some scenarios, say that adversaries create a vault, quickly increase the share price by "donating" assets, deceive some lending protocols to believe the high share price, and then atomically withdraw all the assets from the vault to reset the price to 1, mint a lot of shares with the initial price, and use them as collateral to borrow much more from the lending protocols while they still believe the earlier high share price? Even if my concern is valid, I'm not sure if this is something that needs to be addressed by this library contract (e.g., not allowing the vault to be emptied, asking some dust leaved to keep track of the share price), or just pitfalls that devs/users should be aware of and operate carefully. Would love to hear from you on this! |
It is not timeout but similar; you can set a higher value for the Our tests need a higher value to cover edge cases. |
Should we set both flags to true and increase the |
Setting However, Please let me know if you want to discuss further! |
We have plans to slightly modify ERC4626 to address (already known and documented) security concerns. |
Oh, that sounds great! In that case, I will open an issue for that for more discussion there. For now, I set |
foundry.toml
Outdated
@@ -1,2 +1,3 @@ | |||
[fuzz] | |||
runs = 10000 | |||
max_test_rejects = 1000000 |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This seems like a high value. Do you know if it's mainly due to vm.assume(false)
which is used in various places?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I see this was mentioned in a previous comment in the PR. I would like to understand more why there are so many rejections, as keeping rejections low is important to keep fuzzing efficient.
If it's due to vm.assume(false)
there may be other ways to implement the same thing, or to prevent the vault call from reverting beforehand.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Yes, that's my understanding based on the document: https://book.getfoundry.sh/cheatcodes/assume
Indeed, it can be reduced to 100K for the current tests (the default is 65536), if you prefer it to be tighter. Please let me know!
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
If the default is 65536, bumping it to 100k doesn't sound like that big of a deal. I'm still wondering if there's a better way to implement this that doesn't rely on vm.assume(false)
so much. It would need to be a way of avoiding triggering reverts, instead of catching them and discarding the run after the fact.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Since the tests are written to be agnostic to the implementation details (so that they are applicable to various implementations), it is hard to precisely find the input range that never reverts for various conditions.
An option is to bound inputs for, e.g., withdraw() using previewWithdraw() or convertToAssets(), but as you know, the preview or convert functions do not need to be precise and they may be conservative, so relying on them may exclude some edge cases. (Indeed, setting _unlimitedAmount
flag to false will exactly do this.)
I haven't found a better way to not exclude edge cases without the trial-and-error approach, but please let me know if we have an idea!
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I see, that makes a lot of sense. Let's lower it to 100k then.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Yep, now it's 100K.
FYI, I opened an issue #3800 for more discussion on my concern that I mentioned above. If the issue is addressed, |
Co-authored-by: Hadrien Croubois <hadrien.croubois@gmail.com>
Co-authored-by: Hadrien Croubois <hadrien.croubois@gmail.com>
This reverts commit 99a220c.
5a1a818
to
34b5736
Compare
Congrats, your important contribution to this open-source project has earned you a GitPOAP! GitPOAP: 2022 OpenZeppelin Contracts Contributor: Head to gitpoap.io & connect your GitHub account to mint! Learn more about GitPOAPs here. |
This PR adds the ERC4626 property tests for standard conformance: https://github.com/a16z/erc4626-tests
The idea is to maintain a common test suite that is applicable for any ERC4626 implementations, to help detect their unintentional discrepancies, if any. More details can be found in this post.
Any feedback or suggestions (e.g., adding more test cases, or improving fuzzing speed, etc.) would be greatly appreciated!
PR Checklist