-
Notifications
You must be signed in to change notification settings - Fork 1.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
feat: commit state changes to test contract during invariant/fuzz tests #3005
Comments
@joshieDo PTAL it seems to be we're missing some context across steps, the timestamp etc isn't persisted from call 1 to call 2 |
To test the cToken exchange rate, you would have to inherit from the cToken contract in your test contract, no? Otherwise, I don't see how this feature proposal could help with asserting time-series invariants. |
You would not need to inherit from the cToken contract. The invariant test contract would store the last exchange rate after each call. Here's an example that might help convey the idea using a nonce counter: https://github.com/mds1/solidity-sandbox/blob/main/test/4_InvariantNonceGoUp.t.sol |
ooh, I see. Thanks for linking to that example, makes sense now. Am I understanding correctly that in your example, the maximum value that If it had done that, the |
Correct!
By "that" do you mean "commit state changes"? If so, the max nonce value in this example is only a function of |
Understanding things is nice.
Yes.
I don't see why this is the case if the state between calls was committed by Forge. Couldn't |
In this nonce example, there are only two functions the fuzzer can call
An invariant test behaves as follows:
The number of times we "call a random method" is given by the invariant Once those 15 calls are made, run number 1 is completed. The state now resets, and we begin run number 2 from the same post- |
Thanks for the explainer but I think we're still talking past one another?
Isn't the very purpose of the feature proposal in this issue to not reset the state? This is what I was getting at with the question from my two comments above.
|
Right, and the state is preserved within the context of a run. Once a run ends, the state is reset to start the next run. A "run" in invariant testing is defined as above, i.e. alternating calls between "check the invariant" and "execute a random call", until I think the confusion is the definition of a "run". In a standard fuzz test, a run is just "a single execution of all the code within your test method". In invariant tests, a run is defined differently. |
I see. It should then follow that the maximum value that the nonce in your example can have given the current version of Forge is 1, because state is not preserved between calls. In your example the maximum nonce would be 15 only after the feature proposal in this issue gets implemented in Forge.
Yeah, that was one bit of confusion, so thanks for the additional color. |
Depends which
It's only the test contracts themselves which don't preserve state between calls of an invariant run |
I was always referring to the storage of the test contract in my comments above, but I see that my wording was ambiguous - thanks for clearing this up. |
After spending a few hours debugging a bug related to this issue, I want to add a few clarificatory remarks to what has been said in this thread.
Actually, any state change brought about in an invariant test (a function that starts with
This works only if the test data is recorded via a targeted contract (e.g. a handler). If you attempt to update the state of this test data contract in an Note: this might have been obvious to you, but it wasn't for me, and I wanted to explain how this works in case others end up in my shoes. The following code snippet shows what I mean above: See code snippet
import { Test } from "forge-std/Test.sol";
import { console2 } from "forge-std/console2.sol";
contract Handler {
uint256 public counter;
function increment() external {
counter++;
}
}
contract Store {
uint256 public counter;
function increment() external {
counter++;
}
}
/*
* Run this test with the following settings:
*
* [profile.default.invariant]
* depth = 5
* runs = 1
*/
contract FooTest is Test {
Handler internal handler = new Handler();
Store internal store = new Store();
function setUp() public virtual {
targetContract(address(handler));
}
function invariant_Foo() external {
// This has a value of 5
console2.log("handler.counter()", vm.toString(handler.counter()));
// This always has a value of 0
console2.log("store.counter() ", vm.toString(store.counter()));
store.increment();
assertTrue(true);
}
}
|
+1 I would also like this feature, think it would help a lot with examining state of invariant test runs. I think while this feature is not yet implemented, it might be worthwhile updating the documentation to make this behavior explicit as it may confuse developers who might be expecting state to persist in the invariant test contract. |
Marking this as closed by |
Component
Forge
Describe the feature you would like
Right now, the state changes made by an invariant test (i.e. the state changes within the test contract itself, timestamp changes, and presumably other
block.*
changes) are not committed to the test contract after each call (for invariant tests) or after the run (at the end of a fuzz test). Without this, there is no way to test time-series types of invariants, such as "contract nonce should only ever increase" or "compound ctoken exchange only ever increases".Committing state changes within a test should have no downside, and will enable testing these types of scenarios.
This feature pairs nicely with #2962 and #2985, to allow devs to get visibility into the intermediate state of the test contract. For example, it would be great to be able to have console.logs such as the ones below (below is annotated with comments for clarity):
Committing state for fuzz tests seems useful too, helpful for avoiding stack too deep to save data to storage and then write a bunch of stuff to a file
The text was updated successfully, but these errors were encountered: