Skip to content

Support verifying a library main contract via a generated harness - #132

Draft
shellygr wants to merge 3 commits into
masterfrom
shelly/library-harness
Draft

Support verifying a library main contract via a generated harness#132
shellygr wants to merge 3 commits into
masterfrom
shelly/library-harness

Conversation

@shellygr

@shellygr shellygr commented Aug 7, 2026

Copy link
Copy Markdown
Contributor

Why

The Prover cannot verify a library as the verification target, and fails silently — there is no error anywhere in certora-cli or the JVM. CalculateMethodParamFilters.kt:182 filters libraries out of parametric rules, so the run reaches the prover, instantiates zero methods, and comes back vacuous.

The Prover itself prescribes the fix: "Calling Library functions from spec is not supported. Use a harness function that calls the library one if needed." This automates it.

How

New certora_autosetup/harnesser/ package. Everything completes before Autosetup.run starts:

detect contractKind == library
  → emit a stub harness in certora/harnesses/
  → probe-build it WITH the library in `files`
  → read the library's allMethods
  → refill the harness, drop the placeholder
  → swap the main contract
AutoSetup then runs once, normally.

Three findings shaped this, each verified against real builds:

  • The library must be listed in the conf's files. An imported-but-unused library is not compiled as its own contract — the build reports its structs and none of its functions.
  • The API comes from allMethods, not internalFunctions. The latter is autofinder instrumentation, and certora-cli skips autofinders for library-hosted functions (certoraBuild.py:2751), so it is systematically empty for libraries. Reading it yields zero wrappers for a library whose surface is all-internal.
  • The fill is not re-entrant into autosetup. setup_prover.py:327 re-seeds from the base build-system dict, so a second run_compilation_analysis would discard every workaround from the first; and a swap inside setup_prover cannot propagate (it returns no handle, ContractHandle is frozen).

Detection uses solc --standard-json with stopAfter: "parsing" — real AST, no build, no import resolution, milliseconds. It keys on contractKind, never on a message, since there is no message.

Codegen

Storage receivers become harness-owned state; CVL keywords are escaped before ABI-collision mangling (atat_, then suffixed); collision suffixes come from the dropped receiver keyed positionally (overloads share a name); storage readers reach through nested structs and mappings; in-place memory mutators get a synthesized return.

Skipped and reported, not silently dropped: private, storage-pointer returns, internal-only types, opaque bytes32 ptr handles. Zero surviving wrappers is a hard error — a method-less target proves nothing.

Coverage

Matches hardhat-exposed's independent counts where they overlap:

Library Wrapped
OZ Math 14/14
OZ EnumerableSet 18/24 6 private
OZ Checkpoints 20/33 incl. the function-type push
OZ StorageSlot refused all 8 return storage pointers
Solady LibBitmap 10/10
Solady LibSort 57/68 in-place mutators
Solady EnumerableSetLib 45/51 both utils/ and g/

Validation

Full local autosetup on OZ EnumerableSet: emits a conf whose verify and parametric_contracts are CertoraLibraryHarness_EnumerableSet, typechecker passes, local prover run succeeds, certoraRun --compilation_steps_only exits 0. A/B against OpenZeppelin's hand-written EnumerableSetHarness.sol — the generated names now read add/remove/contains/at_Bytes32Set/Bytes32Set_inner_indexes against the human's add/remove/contains/at_/_indexOf; the remaining differences are the two extra set types the human harness does not cover.

Tests live in the Autosetup repo (29 unit tests over real trimmed build fixtures); needs a submodule pin bump there once this merges.

Known limitation

Solady's EnumerableSetLib sets are struct { uint256 _spacer; } with assembly-derived slots, so the owned state variable may not model where the data actually lives. Treat its 45/51 as unproven until a prover run confirms it.

🤖 Generated with Claude Code

shellygr and others added 3 commits August 7, 2026 20:56
The Prover skips libraries when instantiating parametric rules, so verifying a
library as the main contract silently proves nothing. There is no error to key
on; the run just comes back vacuous.

certora_autosetup/harnesser generates a plain contract exposing one public
wrapper per library function, which the caller verifies instead:

- detect: solc --standard-json with stopAfter "parsing" reads contractKind
  without resolving imports or building, so detection costs milliseconds.
- run: emit a placeholder harness, probe-build it alongside the library, read
  the library's API, then refill the file. The library is listed in the build's
  files explicitly, because an imported-but-unused library is not compiled as
  its own contract and the build then reports none of its functions. The
  placeholder carries one external function since a method-less contract is
  dropped by contract discovery and by the signature database.
- read_build: the API comes from allMethods (external + internal + private),
  not internalFunctions, which holds autofinder instrumentation and is
  systematically empty for libraries. A library is located by (name, file):
  Solady ships 17 library names twice with differently scoped structs.
- plan: classify each function, own a state variable per storage receiver,
  escape CVL keywords before mangling ABI collisions, derive storage readers,
  and synthesize a return for in-place memory mutators.

render_wrapper_contract grows a parentless mode (a library cannot be a base
contract) and a slot for file-scoped pragmas.

Measured against the corpora, matching hardhat-exposed's independent counts
where they overlap: OZ Math 14/14, EnumerableSet 18/24, Checkpoints 20/33,
StorageSlot refused (all 8 return storage pointers); Solady LibBitmap 10/10,
LibSort 57/68, EnumerableSetLib 45/51.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Both entry points swap the main contract before anything derives state from
its name. The name reaches the sanity spec, the base conf, the verify target
and the result keys, so swapping later would leave those naming a contract the
Prover instantiates no methods against.

- autosetup CLI swaps right after the main handle is parsed.
- composer swaps ahead of SourceFields, so component analysis, CVL authoring
  and the conf's verify target all agree on one name — AutoSetup keys its
  returned summary and config by that name too.

The library stays in the scene beside the harness: the wrappers call into it,
and the build only reports a library's own functions when it is named as a
file in its own right.

The LLM harness agent owns certora/harnesses and rewrites every entry it is
given, so it now skips files carrying the generated-harness sentinel — that
file is the verification target, not a wrapper to be improved on.

Verified end to end: autosetup on OZ EnumerableSet emits a conf whose verify
is CertoraLibraryHarness_EnumerableSet and whose files list both the harness
and the library.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Every storage receiver belongs to the library being harnessed, so repeating its
name in each identifier only made them long: at_EnumerableSet_Bytes32Set and
certoraStore_EnumerableSet_Bytes32Set_inner_indexes where the hand-written
harness beside it writes at_ and _indexOf.

Names now read at_Bytes32Set, length_Bytes32Set, _certoraStore_Bytes32Set and
Bytes32Set_inner_indexes. A type from outside the library keeps its qualifier,
which is what still tells it apart. Readers are named for the type and member
path rather than the state variable, so they read as accessors.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
from certora_autosetup.harnesser.run import ensure_library_harness


def _split_target(target: str) -> tuple[str, str]:

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

to Claude: I'm surprised we don't have such a utility function yet

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant