fix(symbolic): explore stateful invariant paths#15591
Conversation
72d63c4 to
6fb335d
Compare
Import setup-time vm.setArbitraryStorage targets into symbolic invariant execution and carry model values for concrete storage slots into replay. This lets invariant counterexamples that depend on symbolic storage behind deployed setup dependencies replay against Forge's concrete executor.
6fb335d to
651c75c
Compare
grandizzy
left a comment
There was a problem hiding this comment.
Found three remaining soundness gaps that can report confirmed symbolic invariant failures outside the concrete campaign semantics. Details and suggested changes inline.
| // step. Preserve the pre-call world together with the | ||
| // reverted branch constraints so end-only and periodic | ||
| // invariant checks observe the same call schedule as the | ||
| // concrete campaign. |
There was a problem hiding this comment.
This restores the pre-call PathState, but concrete top-level reverts retain non-journaled effects such as vm.mockCall and vm.warp. A reverting handler can therefore affect the next concrete call while symbolic execution drops that effect and may report Safe.
| let resolved_name = | ||
| invariant_handler_failure_name(identified_contracts_ro, reverter, selector); | ||
| let symbolic_storage = | ||
| symbolic_handler_storage.get(site).map(Vec::as_slice).unwrap_or(&[]); |
There was a problem hiding this comment.
This storage is keyed only by handler site, but the following fuzz campaign can replace the seeded symbolic failure with a shorter sequence at the same site. We then attach stale storage to a different sequence and persist it as replay-confirmed.
|
One replay-integrity issue remains after the latest fix: fresh symbolic artifacts now record an exact That means a stale artifact can still be accepted when the original predicate no longer fails but the same sequence fails in The persisted replay should validate and preserve the concrete failure site, and artifact replay should reject missing site metadata as unverifiable. Proposed diff: #15591 (comment) |
There was a problem hiding this comment.
I think these previous comments are not fully addressed:
#15591 (comment)
#15591 (comment)
#15591 (comment)
Unsure we need to block on them, but i wanted to pin them at least for follow-up.
Keep symbolic replay storage paired with the exact handler call sequence that produced it. If fuzzing later selects a different reproducer at the same failure site, persist that concrete sequence without stale symbolic storage.
Description
This improves symbolic invariant execution for stateful Morpho-style paths. It lets bounded symbolic invariant exploration continue through multi-call sequences, replays setup-time arbitrary-storage values for explicit symbolic-storage targets, and completes hard-arithmetic fallback models with the supporting storage values needed by checked ERC20-style arithmetic guards.
supply -> supplyCollateral -> borrowsequence. OnGalloDaSballo/morpho-foundry-fv@example-concrete-fallback, the clean harness pins the market/token/oracle addresses, but it does not provide a concrete target path that funds/approves tokens or sets an oracle price. Normal invariant fuzzing confirms that boundary:invariant_hasBorrowedpassed after 256 runs with all 128000 target calls reverting.forge test --symbolicon the same repro reaches the relevant shape but stops incomplete after 26 paths withhard arithmetic heuristic witness used; no replayed counterexample found.loanToken,collateralToken, andoracleas arbitrary storage, this PR finds and replays the 3-callsupply -> supplyCollateral -> borrowcounterexample (paths: 177,models: 1,hard-arith: 84in a fresh run).Current workaround: explicit dependency storage boundary:
vm.setArbitraryStorage(address)when a symbolic invariant depends on setup-deployed dependency contracts whose storage must be satisfiable, but no target call in the invariant campaign writes that storage. In the Morpho repro, that means the ERC20 token mocks and oracle mock:What this intentionally does not do:
--symbolic-storage-layout genericproduces a 2-path counterexample that does not replay because it also makes harness/protocol state arbitrary. That is too broad for this class of invariant test.Status quo
svm.enableSymbolicStorage/ Foundryvm.setArbitraryStorage), so it is not a better no-annotation UX for this case.MorphoHarness,requireassumptions,extSloads => NONDET,_.price() => CONSTANT, and envfree getters instead of asking the prover to infer arbitrary dependency storage.