Auto-enable dynamic_bound/dynamic_dispatch when contract creation is detected#48
Auto-enable dynamic_bound/dynamic_dispatch when contract creation is detected#48shellygr wants to merge 3 commits into
Conversation
Minimal-proxy clones deployed via OpenZeppelin's Clones library (EIP-1167) are only resolvable by the prover dynamically, not statically -- without dynamic_bound/dynamic_dispatch, calls on a freshly-cloned instance hit "ASSUME false[optimistic dispatcher]" and every path through the creating function reverts, producing a vacuous sanity check. Nothing defaulted these on before, so every Clones-using project needed a manual conf edit. Detect Clones.clone/cloneDeterministic/cloneWithImmutableArgs calls (both direct Clones.clone(...) and `using Clones for address` attached-call forms) purely via the already-parsed solc AST -- no regex/string-matching on source text. Wired in as its own phase in setup_prover.py, mirroring the existing ERC-7201 detection phase's "detect -> set flag on updated_config_dict" idiom. Each key is skipped independently if the user already controls it, via a pre-populated config_dict entry or a raw --dynamic_bound/--dynamic_dispatch token (either "--flag value" or "--flag=value" form) in --extra-args. Verified end-to-end against the real-world case that motivated this: running certora-autosetup against lidofinance/stonks's BuybackExecutor (which depends on Stonks.sol's Clones.clone() call in placeOrderWithAmount()) produces a base conf with dynamic_bound/dynamic_dispatch set automatically, no manual edit needed. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
| # generate_ast_graph already logged a WARNING on parse failure; don't let | ||
| # that outage silently swallow Clones detection too -- reload independently. |
There was a problem hiding this comment.
why would it succeed in this case as opposed to failing in generate_ast_graph?
There was a problem hiding this comment.
Claude answers: You were right that it wouldn't — a JSON re-parse of the same file would fail exactly like generate_ast_graph's own parse did; the only cases the fallback actually covered were post-parse failures inside generate_ast_graph (parent-graph build or the graph-file write). Restructured in 24e9019: all_asts.json is parsed once in process_certora_build_json and the parsed dict is passed to both generate_ast_graph (signature now takes the dict) and creation detection, so the fallback is gone entirely. A parse failure now degrades both consumers to a single warning; a parent-graph build/write failure no longer affects detection at all.
Replaces the OpenZeppelin-Clones-specific detector with an instruction-level one (creation_detection.py): per compilation unit, every FunctionDefinition/ ModifierDefinition/ContractDefinition subtree is scanned for Yul create/create2 and typed `new C()`, and creation kinds are propagated up the intra-unit call graph (referencedDeclaration edges from call sites and modifier invocations) to a fixpoint; roots are nodes in in-scope files plus linearized base contracts. Covers Solady LibClone, clones-with-immutable-args, CREATE3 wrappers and hand-rolled factories without naming any library. ContractDefinition nodes aggregate their embedded members, so constructor-time creation outside any function (state-variable initializers, inheritance-specifier base-constructor arguments) is caught too. Flag gating is now kind-aware: any creation defaults dynamic_bound=1; only runtime-assembled bytecode (assembly create/create2) additionally defaults dynamic_dispatch, since a typed `new C()` instance resolves to a clone of C under dynamic_bound alone and keeps static call resolution. all_asts.json is now parsed once in process_certora_build_json and shared by generate_ast_graph (signature changed to take the parsed dict) and the detector -- the previous re-parse fallback would have failed exactly like the original parse, so it is gone. Hardening from an adversarial review pass over real solc 0.5.17-0.8.25 output: iterative subtree walk (deep machine-generated expressions previously hit Python's recursion limit and failed the whole compilation-analysis phase), and a loud warning on solc<0.6 unstructured inline assembly, where raw creates are invisible to the structured-Yul scan. clones_detection.py is superseded, no longer imported, and removed in a follow-up commit. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Replaced by creation_detection.py in the previous commit; nothing imports it. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
There was a problem hiding this comment.
alternatively, we could make this part of call resolution, add a hook on CREATE and CREATE2 opcodes that asserts and match on that in order to enable the flags, instead of relying on brittle AST parsing
SG: The standard way we dealt with Clones.clone and others in the past is NONDET'ing them. However there are cases where this solution limits the verification scope too much. In such cases it can be better to enable dynamic dispatch. If callers know they don't need to call the cloned contracts and reason about them it is possible to go back to NONDET. Opting for the more precise option here.
TODO: Consider if to move this to call_resolution, as it already gives out info about contract creation
Summary
dynamic_boundset; without it, calls on a freshly created instance may fail withASSUME false[optimistic dispatcher]and every path through the creating function reverts.FunctionDefinition/ModifierDefinition/ContractDefinitionsubtree of the already-parsed solc AST is scanned for Yulcreate/create2and typednew C(), and creation kinds are propagated up the intra-unit call graph (referencedDeclarationedges from call sites,FunctionCallOptions-wrapped calls, and modifier invocations) to a fixpoint. Roots are nodes in in-scope files plus every base contract an in-scope contract linearizes over. This covers OpenZeppelinClones, SoladyLibClone, clones-with-immutable-args, CREATE3 wrappers, and hand-rolled factories with one mechanism — including creation living outside any function (state-variable initializers, inheritance-specifier base-constructor arguments) and creation reachable only through out-of-scope library code.dynamic_bound=1; only runtime-assembled bytecode (assemblycreate/create2, i.e. proxy/clone patterns) additionally defaultsdynamic_dispatch— a typednew C()instance resolves to a clone ofCunderdynamic_boundalone, keeping the more precise static call resolution.--dynamic_bound/--dynamic_dispatchtoken in--extra-args, both space and=forms).all_asts.jsonis parsed once and shared by the AST-parent-graph builder and the detector.Known limitations (documented in the module docstring)
referencedDeclaration, so creation reachable only that way is missed (inherent to a static reference-based call graph).operationsstring is possible if desired — left out pending maintainer sign-off.