Skip to content

certora-fv-amenability: static FV-amenability scorer (phase 1)#87

Draft
shellygr wants to merge 13 commits into
masterfrom
shelly/fv-amenability
Draft

certora-fv-amenability: static FV-amenability scorer (phase 1)#87
shellygr wants to merge 13 commits into
masterfrom
shelly/fv-amenability

Conversation

@shellygr

Copy link
Copy Markdown
Contributor

What

New module certora_autosetup/amenability/ + CLI certora-fv-amenability: score how amenable a Solidity project is to automatic formal verification with autosetup —

  • low — needs a full reference implementation; a small rewrite won't suffice
  • medium — scoped configuration/customization needed to get the automatic proof going
  • high — expected to pass autosetup as-is

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 function AnalysisContext -> SignalResult with a score in [0,1] (1 = amenable), raw counters, and evidence carrying project-relative file:line:

signal catches
asm_density inline-assembly saturation
asm_fp_manipulation mstore(0x40, …) — free-memory pointer as a first-class object
asm_trampoline assembly/low-level delegatecall forwarding layers
bitmask_style wide-mask bit surgery inline vs behind small internal accessors
function_length 150+-line functions (no divide-and-conquer seams)
unchecked_nonlinear unchecked nonlinear arithmetic with symbolic operands
mixed_theory bitvector + nonlinear ops interleaved in one function
curated_summary_hits hand-rolled mulDiv/sqrt/… vs libraries with curated summaries (reads setup/function_summaries.json)
storage_packing sload/sstore on computed slots (incl. via Yul locals) — hand-rolled layouts that break storage analysis
external_call_surface low-level call counts / try-dispatch
dynamic_loops storage-length- and symbolically-bounded loops
surface_shape scene-size normalizers (weight 0)

Aggregation = 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 (a certoraRun --dump_asts output) or discovers one under .certora_internal/; with neither it exits 1 with a distinct does-not-compile JSON 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): PackedBook trips every signal on purpose — each per-signal test asserts detection, evidence anchoring, and function attribution — while the CleanVault control (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 with tests/fixtures/amenability/generate.py (dev-only).
  • Validated on a real protocol of the motivating class (heavy assembly trampolines, hand-rolled packed storage, mixed-theory math, 500+-line functions — empirically un-provable without prover-flag surgery): scores low (0.12) with 87 evidence items pinpointing each pathology; the benign fixture scores high (0.94).
  • pyright: 0 errors on the module.

🤖 Generated with Claude Code

shellygr and others added 13 commits July 15, 2026 18:18
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>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant