Skip to content

feat(edsl): journal linked external calls in the executable plane - #2334

Open
Th0rgal wants to merge 2 commits into
mainfrom
feat/external-call-observability
Open

feat(edsl): journal linked external calls in the executable plane#2334
Th0rgal wants to merge 2 commits into
mainfrom
feat/external-call-observability

Conversation

@Th0rgal

@Th0rgal Th0rgal commented Aug 15, 2026

Copy link
Copy Markdown
Member

Summary

The EDSL executable plane's linked-call primitives (externalCallBind, callResultWords, tryExternalCallWords, and the safeTransfer family) were pure () no-ops: executable-plane theorems quantified over ContractState.calls could not observe whether a call happened at all, so double-send, omission, reorder, rename, and argument mutants were indistinguishable at run level.

This PR makes each primitive append a name-keyed ExternalCall entry to ContractState.calls:

  • Verity.Core.ExternalCall gains a defaulted name : String := "" field (existing address-keyed model-plane entries unaffected).
  • Contracts.linkedCallEntry builds the entry: exact argument words as calldata, .success/.failure control, in-band stub returndata; externalCallStubWord is now public and externalCallStubSuccess reserves the "fail" callee for the failure path.
  • All run-lemmas are @[simp] … := rfl; the compiler pipeline consumes model bodies (IR constructors), so Yul generation and difftests are unaffected.
  • New smoke module Contracts/Smoke/ExternalCallObservability.lean runs the programs and separates every mutant class from the original over an arbitrary pre-state: duplicated call, omitted call, reordered calls, renamed callee, zeroed argument, and the safeTransfer double-send form. It also proves a monadic revert rolls the journal entry back through Contract.run's snapshot semantics (EVM top-level revert observability).
  • TRUST_ASSUMPTIONS.md documents the remaining boundary (stub-not-adversary; the pure externalCallWords form stays non-journaling; unmodeled callExternal/mapping stubs); AUDIT.md records the new axiom-free surface.

Adopted from an abandoned unpushed WIP based on 0fd1c2f8 (= current main), completed with compile fixes: to is a reserved token in the Lean grammar (binders renamed toAddr), a no-progress simp only removed, and the "fail"-callee example restated as a full ContractResult equality to avoid a missing Inhabited (Call.Result Uint256) instance.

Campaign context: continues the bounded Verity→Lido closure campaign from milestone minimal-11 25c82e13, with the Lido model pinned to Verity main 04729a9 (Lido PR #69 remains review-first and unmerged). A vacuous externalCallBind := pure () was explicitly ruled out as a TX close; this PR is the non-vacuous repair.

Test Plan

  • lake build — exit 0 (2471 jobs)
  • lake build Contracts PrintAxioms Compiler.CompilationModelFeatureTest — exit 0 (2695 jobs); zero sorry in the log; no warnings in touched files
  • Zero new proof escapes: git diff | grep -E '^\+.*(sorry|admit|\baxiom\b|unsafe|native_decide|@\[implemented_by)' → no matches (also checked the new smoke file)
  • make check — green except test_profile_ci_resources (container lacks GNU /usr/bin/time; environmental, unrelated to this diff)
  • artifacts/verification_status.json regenerated (core_lines 1098→1103)
  • Foundry difftests (FOUNDRY_PROFILE=difftest forge test) — forge unavailable in this container; deferred to CI
  • Exact-head CI (verify.yml) on this PR head

Related Issues

Addresses part of #2084 (Tier 4 — external calls & low-level mechanics): run-level observability of the executable linked-call family.

🤖 Generated with Claude Code


Note

Medium Risk
Changes executable-plane semantics and ExternalCall shape used by proofs and stubs, but the denotational model plane is untouched and behavior is bounded by deterministic stubs plus documented trust boundaries.

Overview
Linked external calls in the EDSL executable plane now append to ContractState.calls, so run-level specs can see whether calls happened, in what order, and with what arguments—instead of the previous pure () no-ops that made double-send, omission, reorder, rename, and argument mutants indistinguishable.

ExternalCall gains a defaulted name field for name-keyed linked callees; linkedCallEntry, recordLinkedCall, and public externalCallStubWord / externalCallStubSuccess (reserved "fail" callee) build journal entries with control, returndata, and ExternalArg.toWords encoding (length-prefixed arrays/bytes, not full EVM ABI). Monadic externalCallBind, callResultWords, tryExternalCallWords, and safeTransfer / safeApprove wrappers journal; ERC-20 writes set target to the token. Pure externalCallWords still does not journal (documented in TRUST_ASSUMPTIONS.md). @[simp] run lemmas and new smoke ExternalCallObservability.lean separate mutant classes over arbitrary pre-states and pin revert rolling back the journal via Contract.run. Model plane unchanged; AUDIT.md updated; verification_status.json core line count bumped.

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

The EDSL executable plane's linked-call primitives (externalCallBind,
callResultWords, tryExternalCallWords, and the safeTransfer family) were
pure no-ops, so executable-plane theorems quantified over
ContractState.calls could not observe whether a call happened at all —
double-send, omission, reorder, rename, and argument mutants were all
indistinguishable at run level.

Each primitive now appends a name-keyed ExternalCall entry (exact
argument words as calldata, success/failure control, stub returndata) to
ContractState.calls, with @[simp] run-lemmas proved by rfl. A new smoke
module runs the programs and separates every mutant class from the
original in an arbitrary pre-state, and shows monadic revert rolls the
journal back through Contract.run's snapshot semantics. The pure
externalCallWords form remains non-journaling and is documented as a
trust boundary in TRUST_ASSUMPTIONS.md; AUDIT.md records the new
axiom-free surface.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
@vercel

vercel Bot commented Aug 15, 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 16, 2026 2:05am

Request Review

@cursor

cursor Bot commented Aug 15, 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_4484e748-16b6-481b-8dd2-fb145e606116)

@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: 4a45b12c4e

ℹ️ 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 Contracts/Common.lean Outdated
let entry := linkedCallEntry name args
(if success then .success else .failure)
(if success then [(externalCallStubWord name args : Nat)] else [])
ContractResult.success (success, (Inhabited.default : α))

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 Return the stub value from successful try calls

For every non-"fail" callee whose stub word is nonzero, this returns a successful result with the wrong payload. For example, tryExternalCall "echo" [7] now returns (true, 0) while journaling successful returndata [7]; consequently TryExternalCallSmoke.tryEcho follows its success branch but stores 0 instead of 7, unlike both externalCallWords and callResultWords. The successful branch should decode externalCallStubWord through ExternalResult rather than always returning Inhabited.default.

Useful? React with 👍 / 👎.

Comment thread Contracts/Common.lean Outdated
{ state with calls := state.calls ++ [entry] }
def externalCallBind {α : Type} [ExternalArg α] (names : List String) (name : String) (args : List α) : Contract Unit :=
fun state =>
let argWords := args.map ExternalArg.toWord

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 contents of dynamic arguments in journal entries

When args contains an Array or ByteArray, this conversion is lossy because the existing ExternalArg instances encode only .size. Thus calls such as externalCallBind [] "notifyArray" [#[1]] and the same call with #[2] produce identical journal entries, despite the commit's stated invariant that altered arguments are observable; the dynamic linked-call forms exercised in ExternalCalls.lean are therefore still unable to reject same-length payload mutations. The journal encoding needs to retain the dynamic contents or their complete ABI representation.

Useful? React with 👍 / 👎.

Comment thread Contracts/Common.lean Outdated
Comment on lines +716 to +718
def safeTransfer (token toAddr : Address) (amount : Uint256) : Contract Unit :=
recordLinkedCall (linkedCallEntry "safeTransfer"
[Verity.addressToWord token, Verity.addressToWord toAddr, amount])

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 Record the ERC-20 token as the call target

For safeTransfer token toAddr amount, the emitted ERC-20 module calls token and supplies toAddr and amount as the transfer calldata, but this entry inherits target := 0 and instead places token in calldata. Unlike ordinary linked externals, this helper's target is dynamic rather than bound by name, so executable proofs inspecting the shared ExternalCall.target or calldata fields observe a wrapper invocation rather than the actual external-call boundary; the safe-transfer family should construct entries with the token address as the target and the ERC-20 arguments as calldata.

Useful? React with 👍 / 👎.

@cursor

cursor Bot commented Aug 16, 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_fc9cb2d0-4e51-4f4a-9420-6fdc3657fc60)

@Th0rgal

Th0rgal commented Aug 16, 2026

Copy link
Copy Markdown
Member Author

@codex review

@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: 9847597736

ℹ️ 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 Contracts/Common.lean
Comment on lines +739 to +741
def safeTransfer (token toAddr : Address) (amount : Uint256) : Contract Unit :=
recordLinkedCall (erc20WriteEntry "safeTransfer" token
[Verity.addressToWord toAddr, amount])

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 Journal the ERC-20 read helpers as well

This adds observability only to the ERC-20 write wrappers, while the adjacent balanceOf, allowance, and totalSupply helpers still return pure stub results without appending to calls. Consequently executable contracts such as ERC20HelperSmoke.snapshotBalance in Contracts/Smoke/ExternalCalls.lean can have these external reads omitted, duplicated, or reordered without the journal detecting the mutation, despite these helpers being monadic external-call crossings. Record entries for the read helpers too, including the token target, arguments, and stub returndata.

Useful? React with 👍 / 👎.

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