Skip to content

feat(edsl): verifiable mixin include for proof reuse - #2327

Merged
Th0rgal merged 4 commits into
mainfrom
feat/verity-mixin-include
Aug 13, 2026
Merged

feat(edsl): verifiable mixin include for proof reuse#2327
Th0rgal merged 4 commits into
mainfrom
feat/verity-mixin-include

Conversation

@Th0rgal

@Th0rgal Th0rgal commented Aug 13, 2026

Copy link
Copy Markdown
Member

Summary

Adds verity_mixin and verity_contract Host include M1, M2 so a host imports mixin Lean definitions and proofs instead of flattening a copied tree (is Parent).

  • Mixin slots stay as written (including ERC-7201 roots).
  • with onlyOwner binds the mixin’s onlyOwner (same StorageSlot).
  • Constructor inits M1(args) M2(args) run mixin constructor values, then the host body.
  • Slot / name / role / modifier / function clashes fail closed.
  • is Parent flatten is unchanged.

Acceptance example: Contracts.Ownable + Contracts.OwnedCounterComposed. Host increment_reverts_when_not_owner reuses onlyOwner_reverts; increment_preserves_owner is WritesOnly + disjoint footprints.

Owned specs now use owner.slot. OwnedCounter remains the Yul / Foundry twin.

Test plan

  • lake build Contracts.Owned
  • lake build Contracts.Smoke (#check_contract + #guard_msgs for slot/name/missing-ctor)
  • lake build Contracts.OwnedCounterComposed
  • Contract.run eval: owner increment writes count, not-owner reverts, owner slot unchanged
  • make check

Note

Medium Risk
Large macro and elaboration surface (include merge, constructors, modifiers) affects how every host contract compiles, though clash checks and smoke tests constrain regressions.

Overview
Adds verity_mixin and verity_contract Host include M1, M2 so hosts import mixin Lean defs and proofs instead of flattening with is Parent. Elaboration merges CompilationModel via mergeIncludedSpecs, emits host_spec plus merged spec, binds with onlyOwner to the mixin modifier, runs Mixin(args) constructor inits, and fails closed on slot/name/role/modifier/function clashes. Mixin slots are not remapped.

Introduces Verity.Specs.Composition (Footprint, WritesOnly, writesOnly_preserves_other_inv) so host proofs reuse mixin guard and invariant theorems when footprints are disjoint.

Ships Contracts.Ownable and OwnedCounterComposed as the acceptance path; Owned specs/proofs switch from literal slot 0 to owner.slot. Docs, CONTRIBUTING mixin guidance, PrintAxioms, and verification manifests are updated. Compiler correctness proofs drop redundant storage simp lemmas; smoke Include.lean adds #guard_msgs for include errors.

Reviewed by Cursor Bugbot for commit afd6501. Bugbot is set up for automated code reviews on this repo. Configure here.

@vercel

vercel Bot commented Aug 13, 2026

Copy link
Copy Markdown

The latest updates on your projects. Learn more about Vercel for GitHub.

Project Deployment Actions Updated (UTC)
verity Ready Ready Preview Aug 13, 2026 3:15pm

Request Review

@cursor

cursor Bot commented Aug 13, 2026

Copy link
Copy Markdown

Bugbot couldn't run - usage limit reached

Bugbot is counted against Cursor usage for this user or team, and this run hit a usage or spend limit.

A user or team admin can review and increase usage limits in the Cursor dashboard.

(requestId: serverGenReqId_f81c16a2-8de8-4bbe-ad84-f4615802e90f)

@github-actions github-actions Bot left a comment

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.

OpenCodeReview first-pass review

🟡 Scout triage only — not a full review. no findings; a human or Codex must still cover the unflagged hunks and proof obligations.

Lean packet budget exceeded: 29 Lean file(s), 1261 changed supported line(s).

Warnings

  • routing : Diff exceeded bounded packet review capability; full OCR not attempted.
OCR pilot metrics & packet coverage

OCR pilot metrics

  • Routing: large-lean-hotspots (router-v10)
  • Changed files: 36 supported / 42 total; Lean 29, trust docs 0, workflow/scripts 5, contracts 0, docs 2
  • Changed lines: 1261 supported; thresholds large Lean >=3 files or >800 lines
  • OCR: status large-lean-hotspots; comments 0; files 0; tokens 0; tool calls 0; warnings 1; duration 0s
  • Largest changed files: Verity/Macro/Translate.lean (+277/-9), Verity/Specs/Composition.lean (+128/-0), Contracts/Smoke/Include.lean (+95/-0), Verity/Macro/Translate/Parsing.lean (+62/-20), Contracts/OwnedCounterComposed/Proofs/Basic.lean (+81/-0)

Packet coverage

  • Packet review: not used; selected 0/8 packet(s)
  • Scout: configured; status skipped_no_packets; model builtin/assistant
  • Scout lenses: provenance, verification-independence, environment-determinism, proof-soundness; rubric items checked 3
  • Strong review: required; status blocked_packet_input
  • Residual risk: Diff exceeded packet budget; use top changed files and deterministic signals as required Codex/human review checklist.
  • Strong packet-review blocker: OpenCodeReview 1.7.9 supports --from/--to full diff ranges, but this workflow does not have a safe packet/window input bridge for Lean hunks yet.

Pilot mode: advisory only. Codex Review remains the merge gate.

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: 52f340711b

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

Comment on lines +1552 to +1555
some {
h with
body := ms.flatMap (·.body) ++ h.body
localObligations := ms.flatMap (·.localObligations) ++ h.localObligations

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

P1 Badge Substitute mixin constructor arguments before merging

When a host initializes a mixin with a differently named parameter or an expression such as M(add x 1), the executable constructor calls the mixin with that argument, but this merge copies the mixin's model body unchanged and retains only the host parameter list. Consequently the compilation model either fails scope validation on the mixin parameter name or, when names happen to match, silently compiles the wrong value; substitute each initializer's arguments into its constructor body before concatenating it.

Useful? React with 👍 / 👎.

Comment thread Verity/Macro/Translate.lean Outdated
Comment on lines +4267 to +4268
let fnExecutableBody ← prependEnumGuards fn.params fnExecutableBody
let fnExecutableBody ← prefixMixinModifierExecutableBody resolvedIncludes fn fnExecutableBody

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

P1 Badge Preserve guard ordering around included modifiers

For a host function that combines an included modifier with requires(...) or an initializer guard, this late prefix makes the executable run the modifier before those generated guards. The model path instead passes the original function to translateBodyToStmtTerms, which places its role/init preludes before the inlined modifier, so executable behavior and the verified/compiled model can disagree on whether a call succeeds and which revert is returned.

Useful? React with 👍 / 👎.

Comment thread Verity/Macro/Translate.lean Outdated
Comment on lines +3504 to +3508
let initNames := inits.map fun (id, _) => toString id.getId
for (mixinName, mixin) in mixins do
if mixin.ctor.isSome then
let shortName := mixinShortName mixinName
unless initNames.any (fun n => namesMixin n mixinName) do

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

P1 Badge Reject duplicate mixin constructor initializers

A host initializer list such as M(x) M(add x 1) passes this validation because it only checks that each name is known and that every required mixin appears. mkHostConstructorDefCommandPublic then executes both occurrences, while mergeIncludedSpecs incorporates M's constructor exactly once, producing a silent executable/model mismatch; require exactly one initializer per included mixin.

Useful? React with 👍 / 👎.

@github-actions

Copy link
Copy Markdown
Contributor
\n### CI Failure Hints\n\nFailed jobs: `build`\n\nCopy-paste local triage:\n```bash\nmake check\nlake build\nFOUNDRY_PROFILE=difftest forge test -vv\n```

@cursor

cursor Bot commented Aug 13, 2026

Copy link
Copy Markdown

Bugbot couldn't run - usage limit reached

Bugbot is counted against Cursor usage for this user or team, and this run hit a usage or spend limit.

A user or team admin can review and increase usage limits in the Cursor dashboard.

(requestId: serverGenReqId_e0dbab4f-9353-4ac2-bfd1-6058972a01f2)

@github-actions github-actions Bot left a comment

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.

OpenCodeReview first-pass review

🟡 Scout triage only — not a full review. no findings; a human or Codex must still cover the unflagged hunks and proof obligations.

Lean packet budget exceeded: 29 Lean file(s), 1417 changed supported line(s).

Warnings

  • routing : Diff exceeded bounded packet review capability; full OCR not attempted.
OCR pilot metrics & packet coverage

OCR pilot metrics

  • Routing: large-lean-hotspots (router-v10)
  • Changed files: 36 supported / 42 total; Lean 29, trust docs 0, workflow/scripts 5, contracts 0, docs 2
  • Changed lines: 1417 supported; thresholds large Lean >=3 files or >800 lines
  • OCR: status large-lean-hotspots; comments 0; files 0; tokens 0; tool calls 0; warnings 1; duration 0s
  • Largest changed files: Verity/Macro/Translate.lean (+400/-12), Verity/Specs/Composition.lean (+128/-0), Contracts/Smoke/Include.lean (+126/-0), Verity/Macro/Translate/Parsing.lean (+62/-20), Contracts/OwnedCounterComposed/Proofs/Basic.lean (+81/-0)

Packet coverage

  • Packet review: not used; selected 0/8 packet(s)
  • Scout: configured; status skipped_no_packets; model builtin/assistant
  • Scout lenses: provenance, verification-independence, environment-determinism, proof-soundness; rubric items checked 3
  • Strong review: required; status blocked_packet_input
  • Residual risk: Diff exceeded packet budget; use top changed files and deterministic signals as required Codex/human review checklist.
  • Strong packet-review blocker: OpenCodeReview 1.7.9 supports --from/--to full diff ranges, but this workflow does not have a safe packet/window input bridge for Lean hunks yet.

Pilot mode: advisory only. Codex Review remains the merge gate.

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: dbe1ad5ab2

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

Comment thread Verity/Macro/Elaborate.lean Outdated
Comment on lines +100 to +101
validateFunctionDeclsPublic (mixinFields ++ fields) errorDecls eventDecls constDecls immutableDecls
externalDecls ctor (mixinModifiers ++ modifiers) functions

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

P1 Badge Preserve mixin declarations when validating modifiers

When an included mixin modifier references a custom error declared by that mixin, this call validates the collected mixinModifiers using only the host's errorDecls (and likewise only host events, constants, immutables, externals, and functions). A mixin that elaborates successfully on its own therefore fails every host include with an “unknown custom error” during validateDoElemsExprTypes; accumulate the corresponding mixin declarations or retain each modifier's original validation context.

Useful? React with 👍 / 👎.

adtTypes := mixins.flatMap (·.adtTypes) ++ host.adtTypes
reservedSlotRanges := mixins.flatMap (·.reservedSlotRanges) ++ host.reservedSlotRanges
slotAliasRanges := mixins.flatMap (·.slotAliasRanges) ++ host.slotAliasRanges
constructor := host.constructor

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

P1 Badge Initialize included mixin immutables in the host constructor

When a mixin declares an immutable, the merged model retains that immutable but this assignment discards the mixin constructor that contains its generated setImmutable statement. expandMixinConstructorForModel only copies the mixin's user-written constructor body, while host spec generation emits initializer statements only for host immutables, so validateImmutableInitialization rejects the composed contract as declaring an immutable that is never initialized. Splice each mixin's generated immutable initialization into the host constructor before retaining only host.constructor.

Useful? React with 👍 / 👎.

Comment thread Verity/Macro/Translate.lean Outdated
Comment on lines +3596 to +3599
for fn in mixin.functions do
unless fn.isInternal do
if seenFnNames.contains fn.name then
throwError s!"include clash: function '{fn.name}' from mixin '{mixinName}' duplicates a host or earlier mixin entrypoint"

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

P2 Badge Namespace or reject colliding internal mixin functions

When two included mixins use the same internal helper name, or a mixin helper matches a host helper, this loop deliberately skips the collision check even though mergeIncludedSpecs later concatenates all internal function specs unchanged. validateCompilationModel then rejects the result with “duplicate internal function name” because Yul definitions are keyed by that name; include internal/shadow names in collision validation or namespace them per mixin.

Useful? React with 👍 / 👎.

Add verity_mixin and verity_contract include so a host imports mixin
Lean defs and theorems instead of flattening a copied tree. with and
constructor inits bind the mixin Contract values; slot/name clashes
fail closed. Ownable + OwnedCounterComposed is the acceptance example.

Owned specs now name owner.slot. is Parent flatten is unchanged.
Codex P1s plus storage clash holes:

- Expand mixin constructor inits into the host CompilationModel with
  hygienic argument bindings; merge no longer concatenates mixin ctor
  bodies (wrong param names / expressions).
- Reject duplicate mixin constructor calls and wrong arities.
- Run mixin modifiers after ABI/init/role guards so executable order
  matches the model.
- Treat transient and persistent slots as distinct; occupy ADT payload
  slots; clash on field aliases; merge reserved/alias ranges.
Validate included modifiers against mixin errors and other mixin
declarations. Emit substituted mixin setImmutable statements in the
host constructor model so merged immutables initialize. Reject
colliding internal helper names at include time instead of failing
later in compilation-model validation.
@Th0rgal
Th0rgal force-pushed the feat/verity-mixin-include branch from dbe1ad5 to e41b80f Compare August 13, 2026 14:57
@cursor

cursor Bot commented Aug 13, 2026

Copy link
Copy Markdown

Bugbot couldn't run - usage limit reached

Bugbot is counted against Cursor usage for this user or team, and this run hit a usage or spend limit.

A user or team admin can review and increase usage limits in the Cursor dashboard.

(requestId: serverGenReqId_bc7404e6-d4bd-4a55-9061-9d5308e0302c)

@Th0rgal

Th0rgal commented Aug 13, 2026

Copy link
Copy Markdown
Member Author

Re-reviewed the latest Codex findings and pushed e41b80f1 (rebased onto main).

Fixed

  • Mixin modifiers now validate/translate against mixin errors, events, constants, immutables, externals, and functions. IncludeErrorHost #check_contract covers a mixin requireError.
  • Host constructor model now emits substituted mixin setImmutable statements, so merged mixin immutables initialize. IncludeImmutableHost #check_contract covers this.
  • Include clash check now rejects colliding internal helpers (and errors/events/constants/immutables/externals/ADTs). IncludeInternalClashRejected is the #guard_msgs case.

The first three Codex P1s (ctor arg substitution, guard order, duplicate mixin ctor inits) were already closed in the previous commit.

Not a silent model rewrite
Internal helper collisions fail closed rather than being namespaced.

@github-actions github-actions Bot left a comment

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.

OpenCodeReview first-pass review

🟡 Scout triage only — not a full review. no findings; a human or Codex must still cover the unflagged hunks and proof obligations.

Lean packet budget exceeded: 29 Lean file(s), 1606 changed supported line(s).

Warnings

  • routing : Diff exceeded bounded packet review capability; full OCR not attempted.
OCR pilot metrics & packet coverage

OCR pilot metrics

  • Routing: large-lean-hotspots (router-v10)
  • Changed files: 36 supported / 42 total; Lean 29, trust docs 0, workflow/scripts 5, contracts 0, docs 2
  • Changed lines: 1606 supported; thresholds large Lean >=3 files or >800 lines
  • OCR: status large-lean-hotspots; comments 0; files 0; tokens 0; tool calls 0; warnings 1; duration 0s
  • Largest changed files: Verity/Macro/Translate.lean (+471/-19), Contracts/Smoke/Include.lean (+209/-0), Verity/Specs/Composition.lean (+128/-0), Verity/Macro/Elaborate.lean (+91/-6), Verity/Macro/Translate/Parsing.lean (+62/-20)

Packet coverage

  • Packet review: not used; selected 0/8 packet(s)
  • Scout: configured; status skipped_no_packets; model builtin/assistant
  • Scout lenses: provenance, verification-independence, environment-determinism, proof-soundness; rubric items checked 3
  • Strong review: required; status blocked_packet_input
  • Residual risk: Diff exceeded packet budget; use top changed files and deterministic signals as required Codex/human review checklist.
  • Strong packet-review blocker: OpenCodeReview 1.7.9 supports --from/--to full diff ranges, but this workflow does not have a safe packet/window input bridge for Lean hunks yet.

Pilot mode: advisory only. Codex Review remains the merge gate.

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: e41b80f1d3

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

Comment on lines +3608 to +3611
let mut seenPersistSlots : Array Nat :=
(host.fields.filter (fun f => !f.isTransient)).flatMap fieldOccupiedSlots
let mut seenTransientSlots : Array Nat :=
(host.fields.filter (·.isTransient)).flatMap fieldOccupiedSlots

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

P1 Badge Reserve executable immutable slots during include checks

When a mixin has an immutable, this check reserves only its declared fields and ignores the hidden field produced by immutableStorageFieldDecl; a host field or another mixin immutable can therefore reuse that slot. For example, the new no-field IncludeImmutableMixin assigns its executable version binding to slot 0 while IncludeImmutableHost.count also occupies slot 0, so the imported getter observes count, whereas the merged compilation model uses the independent bytecode immutable. Include these generated immutable fields in the collision check so executable proofs cannot diverge from the compiled contract.

Useful? React with 👍 / 👎.

Comment on lines +3709 to +3712
let localUsed := fn.modifiers.filter fun id =>
localModifiers.any (fun m => m.name == toString id.getId)
let mixinUsed := fn.modifiers.filter fun id =>
mixinModNames.contains (toString id.getId)

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

P1 Badge Preserve declared order across local and mixin modifiers

When a host function interleaves modifier kinds, such as with localGuard, mixinGuard, partitioning the list here causes localGuard to be inlined into the body while every retained mixin modifier is later prefixed around that body. Both executable and model lowering therefore run mixinGuard before localGuard, reversing the source order and potentially changing which guard reverts or which side effects occur; inline the combined modifier sequence without separating its ordering.

Useful? React with 👍 / 👎.

Rebase onto main rebuilt TypedIRCompilerCorrectness against lenses-off
default simp. The explicit readSlot/writeSlot simp arguments are now
unused and fail the warning baseline.
@Th0rgal
Th0rgal merged commit b8f2bdf into main Aug 13, 2026
6 of 7 checks passed
@cursor

cursor Bot commented Aug 13, 2026

Copy link
Copy Markdown

Bugbot couldn't run - usage limit reached

Bugbot is counted against Cursor usage for this user or team, and this run hit a usage or spend limit.

A user or team admin can review and increase usage limits in the Cursor dashboard.

(requestId: serverGenReqId_fd04d610-043d-4825-be00-475a76c66168)

@github-actions github-actions Bot left a comment

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.

OpenCodeReview first-pass review

OCR did not run — this diff has no OCR-supported files. Not a review outcome.

No changed files matched OCR include rules.

OCR pilot metrics & packet coverage

OCR pilot metrics

  • Routing: no-supported (router-v10)
  • Changed files: 0 supported / 0 total; Lean 0, trust docs 0, workflow/scripts 0, contracts 0, docs 0
  • Changed lines: 0 supported; thresholds large Lean >=3 files or >800 lines
  • OCR: status no-supported; comments 0; files 0; tokens 0; tool calls 0; warnings 0; duration 0s

Pilot mode: advisory only. Codex Review remains the merge gate.

Th0rgal added a commit that referenced this pull request Aug 13, 2026
#2327 (verity-mixin-include) landed proofs relying on the pre-#2326
storage-lens simp set; four compile_*_semantics theorems no longer fold
the raw storage record into writeSlot. Unfold ContractState.writeSlot in
their closing simp sets (only the four affected sites; the other
occurrences stay untouched to keep the zero-warning baseline).
Th0rgal added a commit that referenced this pull request Aug 15, 2026
Reconciles the pre-rebase public head of PR #2329 with the branch as
replayed onto main 0fd1c2f. Uses -s ours deliberately: the tree is taken
entirely from the replayed side, which already contains this work.

Nothing is discarded by this strategy:
- eb46f61 (the genuine C5 step 2.5 payload) was replayed as b84d0fa with
  an identical patch-id (66a10d9a).
- 52f3407, dbe1ad5 and 3d65643 had already landed on main via #2327 and
  #2332; main carries the later reviewed fixes on top (e41b80f, f784f75),
  so main's version supersedes them.

This junction preserves both histories and keeps the push fast-forward, so
the branch is updated without rewriting published history.
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