Skip to content

autosetup: keep the named main contract in the scene - #165

Open
shellygr wants to merge 2 commits into
masterfrom
shelly/main-contract-in-scope
Open

autosetup: keep the named main contract in the scene#165
shellygr wants to merge 2 commits into
masterfrom
shelly/main-contract-in-scope

Conversation

@shellygr

Copy link
Copy Markdown
Contributor

The bug

certora-autosetup --main-contract <path>:<Name> can fail with

Main contract 'X' is not among the compiled contracts in the prover scene: [...].
Most often this means 'X' is abstract or an interface ...

for a contract that is neither abstract nor an interface, and that the build compiled
perfectly well. The message describes compilation, but the decision was made much earlier,
in scoping:

  • auto_detect_contracts filters out every file whose path contains a dependency segment
    (node_modules/, lib/, forge-std/, dependencies/, .git/solidity_utils.py:13).
    That is right for third-party code and wrong for a repo that keeps deployed sources inside
    a bundle directory, e.g. a soldeer-style src/<Component>/dependencies/<pkg>/src/Foo.sol.
  • deduplicate_contract_handles keeps the shortest path per contract name, so a same-named
    contract elsewhere in the tree can displace the file the caller named.

cli.main never reconciled the two: contract_handles came from detection, main_contract_handle
was parsed separately, and nothing guaranteed the second was in the first. Only the whole-scene
conf reached setup_prover, so the main contract was simply absent from files.

The fix

with_main_contract(handles, main_handle) — a contract the caller named is in scope by
definition. It displaces a same-named handle from another file rather than sitting beside it:
the caller said which file it meant, and keeping both restores exactly the ambiguity dedup
exists to remove.

Also: the main contract now goes through the same artifact-backed name resolution as
--contract-files-and-name, so a bare path.sol spec resolves to the contract the file really
declares instead of the filename stem — previously that mismatch produced the same late failure.

Tests

Three cases in tests/test_contract_utils_nested_project.py: a main contract detection missed,
a same-named handle displaced, and an already-detected main contract left untouched.

pytest -m "not expensive": 698 passed, 9 skipped. pyright: 0 errors.

Found while running a repo with a nested Foundry project through the mass-test rig, where the
main contract lives under a bundle's dependencies/ directory. Independent of #164, which fixes
a remapping-context defect in the same repo shape; both are needed for that layout to work.

Two upstream steps can drop the contract the caller named. Auto-detection skips every
file under a dependency directory (node_modules/, lib/, dependencies/, ...) — which is
where per-address verification bundles and vendored sub-projects keep real, deployed
code — and deduplication prefers the shortest path when two files declare the same
contract name. The main contract then never reaches the compilation conf, and the run
dies much later inside setup_prover with "'X' is not among the compiled contracts in
the prover scene", a compilation message for what is really a scoping decision.

with_main_contract() guarantees its presence, displacing a same-named handle from
another file: the caller said which file it meant, and keeping both would put back the
ambiguity dedup exists to remove.

The main contract now also goes through the same artifact-backed name resolution as
--contract-files-and-name, so a bare `path.sol` spec gets the contract the file really
declares rather than the filename stem.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
@shellygr

Copy link
Copy Markdown
Contributor Author

Full-corpus verification (313 projects, 569 contracts)

Cloud sweep full-2026-08-14 on master + #164 + this branch, PreAudit master; baseline
full-2026-08-07 (master @ Aug 1 + PreAudit @ Aug 1).

baseline this run
contracts ok 378 / 568 (66%) 390 / 569 (68%)
main-not-in-scene 41 0
missing-deps/submodule 28 54

main-not-in-scene is gone as a failure class. Most of those contracts do not turn green — they
move to missing-deps/submodule, which is the point: the main contract now reaches the compilation
conf, so the run fails (when it fails) on the project's actual missing dependencies instead of on a
scoping decision dressed up as a compilation error. 14 rows went failed → ok outright, 8 of them
from main-not-in-scene.

The nested-project corpus entry that motivated both PRs passes end-to-end through the ordinary
autosetup path, main contract included:

[209/569] ok   <entry>:<main contract under a bundle's dependencies/>   (473s)
[481/569] ok   <entry>:<sibling bundle contract>                        (318s)

3 regressions, none attributable to either PR:

  • 2 × one project's contracts → missing-deps/submodule. Its import dep/src/contracts/…/Math.sol
    is now rewritten to node_modules/dep/src/contracts/src/contracts/… — a doubled prefix from a
    remapping forge remappings did not emit in the baseline. Cause is toolchain drift: the fleet
    image installs Foundry unpinned, so this image carries forge 1.8.0-nightly (2026-08-13) where the
    baseline image did not. No context-scoped remapping is involved and the contract was already in the
    detected scene, so neither PR is in the path.
  • 1 × timeout at the 60-minute cap; a sibling contract in the same project took 58 minutes on this
    run, so it is the cap, not a new failure.

Caveat on attribution: the baseline image is two weeks older, so #114, #116, #83, PreAudit master and
the Foundry bump all ride along with these two PRs. Regressions are the trustworthy signal in that
kind of diff, and the three above are accounted for.

Separately worth pinning: the fleet image should fix a Foundry version, or every rebuild introduces
this class of confound.

@shellygr
shellygr requested a review from jar-ben August 18, 2026 22:45
return result


def with_main_contract(

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Please rename the function to something like with_contract_handle and params handles and to_add. The functionality of this function is not specific to a "main" contract. Also, the comment should just describe what the functions does, i.e. adds handle if not already presented, and that it drops other handles that shared the .contract_name.

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.

Claude answers: Done in 1f1771f. It is now with_contract_handle(handles, to_add), and the docstring only describes behaviour: it returns the list with to_add appended if it is not already there, and drops any handle carrying the same contract_name but a different source file, so the returned list names each contract once. The reasoning about auto-detection skipping dependency directories and dedup preferring the shortest path moved to the call site in cli.py, which is the place that is actually about the main contract.

Comment thread certora_autosetup/autosetup/cli.py Outdated
f"— adding it to the scene",
"INFO", "Autosetup",
)
contract_handles = with_main_contract(contract_handles, main_contract_handle)

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

tiny optimisation, but also more logical code - shouldn't we call with_main_contract only in the above if branch? i.e. when main_contract_handle not in contract_handles

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.

Claude answers: Moved inside the if. Same result either way, since the helper returns the list untouched when the handle is already present, but the call now sits next to the condition that explains it. 1f1771f

…lies

The helper never depended on the handle being the main contract, so it is now
with_contract_handle(handles, to_add) and its docstring says what it does: append the handle
if it is missing, and drop any other handle carrying the same contract_name.

Why the main contract can go missing belongs at the call site, where the main contract is
what we are talking about, so that reasoning moved into cli.py. The call moved into the
branch that already tests for absence: the helper returns the list untouched otherwise.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
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.

2 participants