certora-fv-amenability: static FV-amenability scorer (phase 1) - #87
certora-fv-amenability: static FV-amenability scorer (phase 1)#87shellygr wants to merge 20 commits into
Conversation
Foundation for a full pydantic model of the solc compact AST as dumped by certoraRun --dump_asts. schema/schema.json is the OpenZeppelin solidity-ast 0.4.62 JSON Schema (MIT, see schema/NOTICE); models will be hand-authored and machine-verified against it. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
…mance tests, fixtures 74 hand-authored pydantic v2 models (Solidity + Yul compact AST), discriminated unions with an UnknownNode fallback per union, single-point forward-ref rebuild, a loader for the 3-level --dump_asts structure (single-parse-per-source, Vyper raw passthrough, per-source degradation), traversal utilities incl. the frozen byte-compatible legacy parent-graph builder, a 151-case schema-conformance suite against the vendored schema, and real .asts.json fixtures generated via certoraRun for solc 0.6.12 / 0.7.6 / 0.8.30. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
…olden test Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
setup_prover: declared-contracts and inheritance extraction go through a shared _iter_contract_declarations (typed find_all with a raw-scan fallback for sources the models cannot parse); generate_ast_graph delegates to the relocated byte-compatible parent-graph builder. auto_munges: .code detection iterates typed MemberAccess nodes (per-node raw salvage on unparsable sources) and decodes src offsets via parse_src. Parity with the legacy raw algorithms is pinned by tests/solidity_ast/test_consumer_migration.py on the real fixtures. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
…enums, cached loads Review findings addressed: iter_nodes_of_type gives typed-first iteration with a raw flat-map sweep for nodes the typed walk cannot reach (e.g. under an unknown future nodeType), used by both migrated consumers so no ContractDefinition or .code MemberAccess can be hidden; InlineAssembly.evmVersion/flags are open strings (a closed fork enum would demote whole files on the next solc release — conformance test carries the allowlisted deviation); .code patches now read and target the source file the offsets refer to (not the compilation unit's main file) and are deduplicated across units; the dump is loaded once per setup run via AstDump.load_cached; union tag sets got a drift-guard test. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
roundtrip_diffs() compares a re-serialized typed tree against the exact source JSON (model_dump with exclude_unset; the internalFunctionIDs de-stamping is the one reversed normalization). Wired into the __main__ validator (with a --json machine mode for corpus sweeps) and pinned on the committed fixtures. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Corpus-driven: real certoraRun fixtures for solc 0.4.26 and 0.5.17 exposed the full legacy dialect, now handled typed (no raw-dict fallback): lenient defaults for fields that did not exist yet (mutability, virtual, tryCall, kind, abstract), string-form documentation and ElementaryTypeNameExpression.typeName, the pre-0.6 InlineAssembly shape (operations text, keyed externalReferences, no Yul AST), file-level EventDefinition (solc >= 0.8.22, missing from the vendored schema), and modeled 0.4-era FunctionDefinition flags. Every deviation from the schema is machine-tracked in the conformance test (LENIENT_REQUIRED / DELIBERATELY_OPEN / FIELD_ALLOWLIST). All five fixtures hold the same strict invariants: fully typed parse, zero unknown nodes, zero unmodeled fields, exact round-trip. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
When the producing solc version is known, a gated field absent at or above its introduction gate fails the source instead of silently reading as None (crash over wrong results); fixtures now always parse with their version so the gates stay exercised. virtual is None below 0.6 (everything was implicitly overridable — False would mislead). effective_mutability/effective_kind derive the pre-gate values faithfully from constant/isConstructor without touching the serialized form. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Empirical solc ladder over every release 0.4.24-0.8.36 pinned the exact gate boundaries: FunctionCall.tryCall from 0.6.0 and InlineAssembly.evmVersion from 0.6.2 (both previously gated later, i.e. under-enforced). solc 0.7.2 alone omits isLValue on enum-member MemberAccess nodes — a bug window, present before and after, so it is lenient (None) rather than gated. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
…solidity_ast stream_ast_files's four call sites collapse into the typed layer: new AstDump.stream_units() (per-compilation-unit typed streaming with unit_filter and solc_version gates) and stream_raw_units() (raw pairs for the byte-compat parent-graph pass, which now accepts streamed units). All three consumers stream instead of whole-file loading; load_cached is gone (the memory win outweighs the shared parse), stream_ast_files removed with its purpose. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
New module certora_autosetup/amenability implementing the FV-amenability metric: score how amenable a Solidity project is to automatic formal verification with autosetup — low (needs a full reference implementation), medium (scoped customization needed), or high (expected to pass as-is). Phase 1 is the deterministic static scorer, built on the typed solidity_ast package: twelve signals over the compact AST (inline-assembly density, free-memory-pointer manipulation, delegatecall trampolines, inline bit-mask surgery vs accessor encapsulation, function length, unchecked nonlinear arithmetic, mixed bitvector+nonlinear theory per function, curated-summary library hit-rate against setup/function_summaries.json, computed-slot storage access, low-level call surface, dynamic loops, and scene-shape normalizers). Aggregation is a weighted mean plus hard rules (per-signal caps, severe-count floor), all tunable in weights.yaml only. Every evidence item carries project-relative file:line. CLI: certora-fv-amenability <project_root> [--ast-dump PATH] — pure JSON in/out so SaaS/CI clients can invoke it directly. Compiling is a hard precondition: with no AST dump available the tool exits 1 with a distinct "does-not-compile" error object; there is deliberately no raw-source fallback scoring. Tests run against a committed dump of a self-authored bait contract that trips every signal (with a clean control contract that must trip none); scoring hard-rules and the CLI paths are covered end-to-end. Validated on a real protocol of the motivating class: it lands "low" with evidence pinpointing each hand-written pathology, while a benign fixture scores "high". Phase 2 (graphcore LLM judge over a versioned rubric) and phase 3 (calibration against labeled autosetup outcomes) follow separately. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
…split - judge/: single structured Opus call over the static report + code excerpts, clamped to at most one level from the static provisional and only with >=2 citations. Rubric is a versioned data file; output records model, rubric version+sha, prompt version, and token usage+cost. Wired via `certora-fv-amenability --judge`. - Assembly signals are now scope-aware: a low-level construct's penalty scales by how well-scoped its containing function is. Assembly confined to a small internal/private helper (the sanctioned pattern) is barely penalized; the same construct interwoven with substantial code in a large or externally-visible function carries full weight. Applies to asm density, free-memory-pointer manipulation, and delegatecall trampolines. - Fixture-backed signal-detection tests move to a separate private test corpus (which mounts this package as a submodule); the pure-unit tests (scoring hard rules, judge guardrails, dump-resolution errors) stay here and run without the fixture. The dump-dependent tests run here too when FV_AMENABILITY_FIXTURES points at a local fixtures checkout. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
…rofile) `low` now requires CO-OCCURRENCE of multiple severe structural killers (delegatecall trampolines, hand-rolled free-memory/storage layouts, monolithic mixed-theory functions) plus a genuinely weak overall picture — the profile that needs a full reference implementation. A project with config-solvable friction (bit-packing, nonlinear math, dynamic loops, one trampoline) is medium, not low. Signal fixes behind the recalibration: - curated_summary_hits only flags hand-rolled NONLINEAR MATH KERNELS (mulDiv/sqrt/mulWad/...); it no longer matches generic names (safeTransfer, toUint*, get/set, extsload) as "hand-rolled math", and a curated library hit now dominates. - dynamic_loops, unchecked_nonlinear, mixed_theory, bitmask_style floor well above zero — they are friction (harder SMT / more config), not rewrite triggers, so bit-heavy or loop-heavy real contracts no longer read as low. - asm_fp_manipulation scales with the amount of interwoven FP surgery instead of cratering on a single write. - Aggregation replaces the low-weighted-mean rule with a structural-killer co-occurrence rule (all thresholds/weights in weights.yaml). Validated against the internal corpus: real protocols that pass autosetup, and verifiable ones autosetup merely needs config for (Comet, EVault, PositionManager class), now score high/medium; the hand-assembled reference-impl profile stays low. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
bc00177 to
514b5e2
Compare
The prover's decompiler unfolds every recursive entry to `-recursionEntryLimit` depth on every path, so a recursive cluster reached from inside a loop multiplies the decompiled program past the block and command budgets before verification starts. A project can be otherwise clean and still be unverifiable because one vendored library implements a bytecode interpreter that way. `callgraph.py` builds the internal call graph of a compilation unit — internal, private, free-function and modifier edges, the ones the decompiler inlines — and finds its recursive clusters with an iterative Tarjan. `mutual_recursion` scores each cluster on size, whether a call site into it sits in a loop, whether it is reachable from a public entry point, and how many distinct sites enter it. Two things make this signal different from the others: - it analyses dependencies as well as project code, because a hazard vendored under lib/ compiles into the same scene; - it is a structural killer, so one severe cluster caps the verdict at medium. A weighted mean cannot express a hazard that is a fraction of a percent of the sources and still fatal. Known limitations are listed in the callgraph module docstring; the notable one is that dispatch is resolved per compilation unit rather than per deployable contract, which can read a template-method pair as a cycle. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
|
Pushed Why it exists. A prod project scored high (0.9766) from this scorer while the Prover could not ingest it at all: a vendored dependency implements a bytecode interpreter with two mutually recursive clusters entered from inside a bounded loop, and the decompiler unfolds each recursive entry to What landed. Two ways it differs from the existing signals, both deliberate:
56 public dump-free tests, pyright clean. Fixture-backed tests are in the private corpus (Certora/fv-amenability-tests#1, also draft, pinned at this commit). Open before this leaves draft: a false positive that fires the veto on recursion-free code. Virtual dispatch is expanded per compilation unit rather than per deployable contract, so two sibling contracts overriding hooks of a shared abstract base get their edges welded into one graph — the template-method / OZ-hooks shape reads as a cycle. Reproduced and measured. The fix is to resolve each virtual call against one concrete contract's linearization and union the per-contract clusters; I did not do it unilaterally since it changes the module's core structure. It is recorded in the I also fixed the adjacent false negative in the same area: override expansion was one level deep, so a grandchild override closing a loop-entered cycle was invisible. Now transitive. |
What
New module
certora_autosetup/amenability/+ CLIcertora-fv-amenability: score how amenable a Solidity project is to automatic formal verification with autosetup —Phase 1 (this PR) is the deterministic static scorer. Phase 2 adds a graphcore-based LLM judge over a versioned rubric data file; phase 3 calibrates the weights against labeled autosetup outcomes. Both follow separately.
Note: branch is based on
shelly/solidity-ast-pydantic(#76) — this PR builds on the typed AST package and should land after it.How
Twelve signals over the typed compact AST (
solidity_ast.find_all/walk), each a pure functionAnalysisContext -> SignalResultwith a score in [0,1] (1 = amenable), raw counters, and evidence carrying project-relative file:line:asm_densityasm_fp_manipulationmstore(0x40, …)— free-memory pointer as a first-class objectasm_trampolinedelegatecallforwarding layersbitmask_stylefunction_lengthunchecked_nonlinearuncheckednonlinear arithmetic with symbolic operandsmixed_theorycurated_summary_hitsmulDiv/sqrt/… vs libraries with curated summaries (readssetup/function_summaries.json)storage_packingsload/sstoreon computed slots (incl. via Yul locals) — hand-rolled layouts that break storage analysisexternal_call_surfacedynamic_loopssurface_shapeAggregation = weighted mean + hard rules (high-severity findings from FP-manipulation / trampoline / storage-packing cap the level at medium; N severe signals force low). All weights and thresholds live in
weights.yaml— calibration never touches code.Compiling is a hard precondition: the CLI takes
--ast-dump(acertoraRun --dump_astsoutput) or discovers one under.certora_internal/; with neither it exits 1 with a distinctdoes-not-compileJSON error. There is deliberately no raw-source fallback.Testing
tests/test_amenability.py(19 tests) runs against a committed AST dump of a self-authored bait contract (tests/fixtures/amenability/signals_bait.sol):PackedBooktrips every signal on purpose — each per-signal test asserts detection, evidence anchoring, and function attribution — while theCleanVaultcontrol (incl. the good accessor-encapsulated bit pattern) must produce zero evidence. Scoring hard rules and both CLI exits are covered end-to-end. Regenerate the dump withtests/fixtures/amenability/generate.py(dev-only).pyright: 0 errors on the module.🤖 Generated with Claude Code