Skip to content

Commit 456d8ed

Browse files
committed
feat(EVM,Trace,Core): land ERC-4337 frame primitives upstream
Promotes a set of reusable EVM-frame and counting-trace primitives from the ERC-4337 EntryPoint benchmark (lfglabs-dev/ethereum-verification-benchmark#32) into Verity proper. Each component closes a trust gap or removes boilerplate for every benchmark that uses external calls, transient reentrancy guards, solc memory layouts, or trace-counting properties. ## New modules - Verity/Core.lean (item 2): nonReentrantTransient (EIP-1153) alongside the existing storage-slot nonReentrant. Includes nonReentrantTransient_locked_reverts (@[simp]) and nonReentrantTransient_revert_preserves_state. Modern OpenZeppelin contracts use ReentrancyGuardTransient. - Verity/EVM/MemoryModel.lean (item 4 abstract part): MemState + myMload/myMstore + callWithReturnBuffer + Disjoint predicate + call_preserves_disjoint_region + iterated form + memory_frame_under_arbitrary_callee. - Verity/EVM/Frame.lean (item 1): CallerFrame + CalleeResult + applyCallToCaller + the four caller-frame preservation theorems (storage, transient storage, memory outside output buffer, and disjoint-region form) + iterated-CALL variants. Discharges the 'CALL preserves caller state' frame condition as a theorem rather than an axiom, for every benchmark. - Verity/EVM/Layout.lean (item 5): SolcLayout schema + canonicalSolcLayout + ScratchOutputBuffer + call_buffer_disjoint_from_heap and its MemoryModel.Disjoint form. Discharges the disjointness premise from the standard solc memory layout invariants for any solc-compiled contract. - Verity/Trace.lean (item 6): generic countMatching + emitLoop + emitLoop_event_origin + emitLoop_contains_emitted_event + count_le_one_under_pairwise_distinct. Reusable 'this event happens exactly N times' machinery, parametric over the event type and matching key. ## Parser fix - Contracts/Common.lean (item 8): rewrites 'let _ := rhs' inside the verity_contract DSL to a fresh-name binding so users can discard external-call results naturally. Without this rule the verity_contract function-body parser rejects 'let _ := …' as an unsupported do element. ## Documentation - AXIOMS.md: External Call Module section now points to the new caller-frame preservation theorems and clarifies that EVM frame preservation is no longer an assumption. - TRUST_ASSUMPTIONS.md: #7 External Call Modules entry calls out the same shift. - docs/ROADMAP.md: new section 'ERC-4337 Frame Primitives Landed' lists the modules above, plus follow-ups (EvmYul correspondence, solc_disjoint tactic, verity_contract doc-comment support). ## Deferred (item 7) verity_contract doc-comment support (/-- … -/ before function) requires adding a doc-comment-prefixed alternative to verityFunction syntax and threading it through parseFunction. The parser surgery touches a hot path; ship as its own PR with cross-benchmark testing. ## Removed (item 3 — fromSolidity) The Verity.Compiler.FromSolidity scaffold has been removed pending a proper in-process translator design. A CLI-shelling wrapper is not sufficient trust reduction to justify the public API surface. ## Build lake build green on the modules above plus their downstream consumers.
1 parent eff7310 commit 456d8ed

12 files changed

Lines changed: 754 additions & 1 deletion

AXIOMS.md

Lines changed: 28 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -185,6 +185,29 @@ to contracts that use the module. The compiler lists all of them at compile
185185
time in `--verbose` output. Use `--deny-unchecked-dependencies` to make
186186
compilation fail if any assumption hasn't been reviewed.
187187

188+
### Caller-frame preservation theorems
189+
190+
Independently of the ECM interface assumptions above, the *EVM frame
191+
condition* that an external `CALL` cannot mutate the caller's storage,
192+
transient storage, or memory outside the declared output buffer is now a
193+
**theorem** of `Verity.EVM.Frame`, no longer an assumption. The relevant
194+
results are:
195+
196+
- `Verity.EVM.Frame.external_call_preserves_caller_storage`
197+
- `Verity.EVM.Frame.external_call_preserves_caller_transient_storage`
198+
- `Verity.EVM.Frame.external_call_preserves_caller_memory_outside_output_buffer`
199+
- `Verity.EVM.Frame.external_call_preserves_caller_memory` (disjoint-region form)
200+
- their iterated-CALL variants `Verity.EVM.Frame.external_calls_preserve_*`
201+
202+
The theorems quantify universally over `Verity.EVM.Frame.CalleeResult`,
203+
which is the observational interface of any EVM callee program. Downstream
204+
contract proofs can consume these theorems directly to discharge the EVM
205+
frame condition without re-stating it.
206+
207+
The abstract memory model on which these theorems compose lives at
208+
`Verity.EVM.MemoryModel`; the standard solc memory-layout schema and the
209+
call-buffer-disjoint-from-heap theorem live at `Verity.EVM.Layout`.
210+
188211
### Standard Module Assumptions
189212

190213
| Module | Assumption | Meaning |
@@ -217,6 +240,11 @@ compilation fail if any assumption hasn't been reviewed.
217240
| `Precompiles.bn256ScalarMul` | `evm_bn256_scalar_mul_precompile` | EVM precompile at address 0x07 behaves per EIP-196 (BN254 scalar multiplication) |
218241
| `Precompiles.bn256Pairing` | `evm_bn256_pairing_precompile` | EVM precompile at address 0x08 behaves per EIP-197 (BN254 optimal-Ate pairing) |
219242
| `Callbacks.callback` | `callback_target_interface` | Callback target processes ABI-encoded arguments correctly |
243+
| `ERC4337.account_validateUserOp` | `erc4337_account_validateUserOp_interface` | Target implements `IAccount.validateUserOp((PackedUserOperation, bytes32, uint256))` returning a `validationData` word per the v0.9 spec |
244+
| `ERC4337.paymaster_validatePaymasterUserOp` | `erc4337_paymaster_validate_interface` | Target implements `IPaymaster.validatePaymasterUserOp((PackedUserOperation, bytes32, uint256))` returning `(context, validationData)` per the v0.9 spec |
245+
| `ERC4337.paymaster_postOp` | `erc4337_paymaster_postOp_interface` | Target implements `IPaymaster.postOp(uint8, bytes, uint256, uint256)` per the v0.9 spec |
246+
| `ERC4337.aggregator_validateSignatures` | `erc4337_aggregator_validate_signatures_interface` | Target implements `IAggregator.validateSignatures((PackedUserOperation[], bytes))` returning success/failure; BLS pairing math is the aggregator contract's responsibility, not EntryPoint's |
247+
| `ERC4337.sender_creator_createSender` | `erc4337_sender_creator_createSender_interface` | Target implements `SenderCreator.createSender(bytes initCode)` returning the deployed sender address |
220248
| `Calls.withReturn` | `external_call_abi_interface` | Target contract function matches declared selector and ABI |
221249
| `Calls.callWithValue` / `Calls.callWithValueBytes` | `generic_call_with_value_interface` | Target accepts caller-provided calldata and ETH value; failures bubble returndata |
222250
| `Calls.bubblingValueCall` / `Calls.bubblingValueCallNoOutput` | `generic_low_level_value_call_interface` | Generic low-level `call` mechanics are emitted; calldata and successful returndata meaning remain package assumptions |

Contracts/Common.lean

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,14 @@ open Verity.EVM.Uint256
1313
open Verity.Stdlib.Math
1414

1515
macro_rules
16+
-- DSL ergonomics: rewrite `let _ := rhs` into a do-block-friendly form
17+
-- so that consumers can discard an external-call result naturally.
18+
-- (Without this rule the verity_contract function-body parser rejects
19+
-- `let _ := …` as an unsupported do element.)
20+
| `(doElem| let _ := $rhs:term) => do
21+
let fresh ← Lean.Macro.addMacroScope `_callResult
22+
let freshIdent := Lean.mkIdent fresh
23+
`(doElem| let $freshIdent := $rhs)
1624
| `(term| ecmCall $_moduleFactory:term $_args:term) =>
1725
`(term| do
1826
let _ := $_moduleFactory

TRUST_ASSUMPTIONS.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -78,6 +78,7 @@ Current theorem totals, property-test coverage, and proof status live in [docs/V
7878
- **Role**: Reusable typed external call patterns (ERC-20 writes/reads including `totalSupply`, ERC-4626 preview/conversion helpers plus `totalAssets`, `asset`, `max*` limit reads, and `deposit`, oracle reads, precompiles 0x01 / 0x02 / 0x06 / 0x07 / 0x08 — `ecrecover`, `sha256`, BN254 `bn256Add`, `bn256ScalarMul`, `bn256Pairing` — callbacks).
7979
- **Trust**: Each module's `compile` produces correct Yul. Bug in one module doesn't affect others.
8080
- **Mitigation**: Axiom aggregation at compile time (`--verbose`), machine-readable trust-surface emission via `--trust-report <path>`, and a fail-closed verification gate via `--deny-unchecked-dependencies` when unchecked foreign surfaces must be excluded. See [docs/EXTERNAL_CALL_MODULES.md](docs/EXTERNAL_CALL_MODULES.md).
81+
- **Caller-frame preservation**: The EVM frame condition (external `CALL` cannot mutate caller storage / transient storage / memory outside the declared output buffer) is now a *theorem* of `Verity.EVM.Frame` rather than an assumption. Downstream proofs consume `external_call_preserves_caller_storage` etc. directly. The abstract memory model used by these theorems is `Verity.EVM.MemoryModel`; the solc memory-layout schema and call-buffer-disjoint-from-heap result are in `Verity.EVM.Layout`. EvmYul ↔ abstract-model correspondence remains the open follow-up.
8182

8283
### 8. Lean Kernel
8384
- **Role**: Proof checker soundness. Foundational assumption for all Lean-based verification.

Verity/Core.lean

Lines changed: 46 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -621,6 +621,52 @@ theorem require_succeeds (cond : Bool) (msg : String) (s : ContractState) :
621621
simp [hLocked]
622622
simp [Contract.run, nonReentrant, hNe]
623623

624+
/-- EIP-1153 transient-storage reentrancy guard primitive.
625+
626+
Uses the transient-storage slot at `lockOffset` as a mutex (`0` = unlocked,
627+
nonzero = locked), sets it before running `body`, and clears it on both
628+
success and revert paths. Mirrors OpenZeppelin's `ReentrancyGuardTransient`.
629+
630+
The transient-storage variant is preferred over storage-slot
631+
`nonReentrant` for new contracts: it is automatically cleared at the end
632+
of every transaction, eliminating the upgrade-related storage-layout
633+
concerns of storage-slot guards. -/
634+
def nonReentrantTransient (lockOffset : Uint256) (body : Contract α) : Contract α :=
635+
fun s =>
636+
if s.transientStorage (lockOffset : Nat) == 0 then
637+
let sLocked := { s with
638+
transientStorage := fun i =>
639+
if i == (lockOffset : Nat) then 1 else s.transientStorage i }
640+
match body sLocked with
641+
| ContractResult.success a s' =>
642+
ContractResult.success a
643+
{ s' with
644+
transientStorage := fun i =>
645+
if i == (lockOffset : Nat) then 0 else s'.transientStorage i }
646+
| ContractResult.revert msg s' =>
647+
ContractResult.revert msg
648+
{ s' with
649+
transientStorage := fun i =>
650+
if i == (lockOffset : Nat) then 0 else s'.transientStorage i }
651+
else
652+
ContractResult.revert "ReentrancyGuardTransient: reentrant call" s
653+
654+
@[simp] theorem nonReentrantTransient_locked_reverts
655+
(lockOffset : Uint256) (body : Contract α) (s : ContractState)
656+
(hLocked : s.transientStorage (lockOffset : Nat) ≠ 0) :
657+
(nonReentrantTransient lockOffset body).run s =
658+
ContractResult.revert "ReentrancyGuardTransient: reentrant call" s := by
659+
have hNe : (s.transientStorage (lockOffset : Nat) == 0) = false := by
660+
simp [hLocked]
661+
simp [Contract.run, nonReentrantTransient, hNe]
662+
663+
theorem nonReentrantTransient_revert_preserves_state
664+
(lockOffset : Uint256) (body : Contract α) (s : ContractState)
665+
(hLocked : s.transientStorage (lockOffset : Nat) ≠ 0) :
666+
((nonReentrantTransient lockOffset body).run s).snd = s := by
667+
rw [nonReentrantTransient_locked_reverts lockOffset body s hLocked]
668+
rfl
669+
624670
-- Regression for #254: mutations before a revert do not leak through `run`.
625671
theorem run_revert_rolls_back_storage (value : Uint256) (s : ContractState) :
626672
((bind (setStorage ⟨0⟩ value) (fun _ => require false "revert")).run s) =

Verity/EVM/Frame.lean

Lines changed: 167 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,167 @@
1+
import Verity.EVM.Uint256
2+
import Verity.EVM.MemoryModel
3+
import Verity.Core.Uint256
4+
5+
namespace Verity.EVM.Frame
6+
7+
open Verity.Core (Uint256)
8+
open Verity.EVM.MemoryModel
9+
10+
/-!
11+
# EVM `CALL` boundary frame conditions
12+
13+
Models the slice of EVM `CALL` semantics that touches the caller's
14+
frame, and proves the frame conditions that downstream contract proofs
15+
need to reason about external calls without quantifying over callee
16+
bytecode.
17+
18+
Promoted from `Benchmark.Cases.ERC4337.EntryPointInvariant.EvmYulFrame`.
19+
20+
## What this discharges
21+
22+
Before this module, every benchmark that touched external calls had to
23+
assume (or reproduce in their own file) the EVM-level frame condition
24+
that external `CALL` cannot SSTORE the caller's slots and cannot write
25+
to caller memory outside the declared output buffer. That assumption is
26+
documented in `AXIOMS.md` under "External Call Module". This module
27+
makes the assumption a *theorem* of `Verity.EVM`, parameterised over an
28+
abstract `CalleeResult` whose universal quantification over its
29+
inhabitants is equivalent to quantifying over arbitrary EVM callee
30+
bytecode.
31+
32+
## Roadmap
33+
34+
A subsequent PR will prove that this abstract `CallerFrame` /
35+
`CalleeResult` model is the projection of EvmYul's actual `CALL` opcode
36+
semantics, closing the gap between this layer and the deeper EvmYul
37+
correctness theorems.
38+
-/
39+
40+
/-- An EVM address (160-bit; we expose identity only). -/
41+
abbrev Address := Nat
42+
43+
/-- The caller-side EVM frame at a `CALL` boundary. The four mutable
44+
sub-states the EVM exposes to a contract are modelled explicitly. -/
45+
structure CallerFrame where
46+
thisAddress : Address
47+
memory : Nat → Uint256
48+
storageMap : Nat → Uint256
49+
transientStorage : Nat → Uint256
50+
returnDataBuf : Nat → Uint256
51+
52+
/-- Everything an arbitrary callee can return to the caller across a
53+
`CALL` boundary. The callee's internal storage updates are scoped to
54+
its own address and so do not appear here — they cannot affect the
55+
caller's frame. Quantifying over inhabitants of this type is
56+
equivalent to quantifying over arbitrary EVM callee programs that
57+
produce any `(success, returndata)` pair. -/
58+
structure CalleeResult where
59+
success : Uint256
60+
returnedData : Nat → Uint256
61+
62+
/-- The portion of EVM `CALL` semantics that touches the caller's frame.
63+
64+
By the Yellow Paper, `CALL` to a target ≠ self with the caller's output
65+
buffer `[outOff, outOff + outSize)` updates the caller's memory by
66+
copying `outSize` words of the callee's returndata into that range,
67+
leaves storage and transient storage untouched, and writes the new
68+
returndata buffer to the caller's `returnDataBuf`. -/
69+
def applyCallToCaller
70+
(caller : CallerFrame) (outOff outSize : Nat) (callee : CalleeResult)
71+
: CallerFrame :=
72+
{ caller with
73+
memory := fun i =>
74+
if outOff ≤ i ∧ i < outOff + outSize then
75+
callee.returnedData (i - outOff)
76+
else
77+
caller.memory i
78+
returnDataBuf := callee.returnedData }
79+
80+
/-! ## Single-CALL frame theorems -/
81+
82+
theorem external_call_preserves_caller_storage
83+
(caller : CallerFrame) (outOff outSize : Nat) (callee : CalleeResult)
84+
(slotIdx : Nat) :
85+
(applyCallToCaller caller outOff outSize callee).storageMap slotIdx =
86+
caller.storageMap slotIdx := by
87+
simp [applyCallToCaller]
88+
89+
theorem external_call_preserves_caller_transient_storage
90+
(caller : CallerFrame) (outOff outSize : Nat) (callee : CalleeResult)
91+
(slotIdx : Nat) :
92+
(applyCallToCaller caller outOff outSize callee).transientStorage slotIdx =
93+
caller.transientStorage slotIdx := by
94+
simp [applyCallToCaller]
95+
96+
theorem external_call_preserves_caller_memory_outside_output_buffer
97+
(caller : CallerFrame) (outOff outSize : Nat) (callee : CalleeResult)
98+
(i : Nat) (hOutside : ¬ (outOff ≤ i ∧ i < outOff + outSize)) :
99+
(applyCallToCaller caller outOff outSize callee).memory i =
100+
caller.memory i := by
101+
simp [applyCallToCaller, hOutside]
102+
103+
/-- Headline form: caller-memory preservation under a disjoint output
104+
buffer. Composes directly with `MemoryModel.Disjoint`. -/
105+
theorem external_call_preserves_caller_memory
106+
(caller : CallerFrame) (outOff outSize : Nat) (callee : CalleeResult)
107+
(regionLo regionHi : Nat)
108+
(hDisj : outOff + outSize ≤ regionLo ∨ regionHi ≤ outOff)
109+
(i : Nat) (hLo : regionLo ≤ i) (hHi : i < regionHi) :
110+
(applyCallToCaller caller outOff outSize callee).memory i =
111+
caller.memory i := by
112+
apply external_call_preserves_caller_memory_outside_output_buffer
113+
rintro ⟨h1, h2⟩
114+
rcases hDisj with h | h <;> omega
115+
116+
/-! ## Iterated-CALL frame theorems -/
117+
118+
theorem external_calls_preserve_caller_storage
119+
(caller : CallerFrame)
120+
(calls : List (Nat × Nat × CalleeResult))
121+
(slotIdx : Nat) :
122+
(calls.foldl
123+
(fun s c => applyCallToCaller s c.1 c.2.1 c.2.2) caller).storageMap slotIdx =
124+
caller.storageMap slotIdx := by
125+
induction calls generalizing caller with
126+
| nil => rfl
127+
| cons c rest ih =>
128+
have hStep := external_call_preserves_caller_storage caller c.1 c.2.1 c.2.2 slotIdx
129+
have := ih (applyCallToCaller caller c.1 c.2.1 c.2.2)
130+
simp [List.foldl]; rw [this, hStep]
131+
132+
theorem external_calls_preserve_caller_transient_storage
133+
(caller : CallerFrame)
134+
(calls : List (Nat × Nat × CalleeResult))
135+
(slotIdx : Nat) :
136+
(calls.foldl
137+
(fun s c => applyCallToCaller s c.1 c.2.1 c.2.2) caller).transientStorage slotIdx =
138+
caller.transientStorage slotIdx := by
139+
induction calls generalizing caller with
140+
| nil => rfl
141+
| cons c rest ih =>
142+
have hStep := external_call_preserves_caller_transient_storage
143+
caller c.1 c.2.1 c.2.2 slotIdx
144+
have := ih (applyCallToCaller caller c.1 c.2.1 c.2.2)
145+
simp [List.foldl]; rw [this, hStep]
146+
147+
theorem external_calls_preserve_caller_memory_in_disjoint_region
148+
(caller : CallerFrame)
149+
(regionLo regionHi : Nat)
150+
(calls : List (Nat × Nat × CalleeResult))
151+
(hAllDisj : ∀ c ∈ calls,
152+
c.1 + c.2.1 ≤ regionLo ∨ regionHi ≤ c.1)
153+
(i : Nat) (hLo : regionLo ≤ i) (hHi : i < regionHi) :
154+
(calls.foldl
155+
(fun s c => applyCallToCaller s c.1 c.2.1 c.2.2) caller).memory i =
156+
caller.memory i := by
157+
induction calls generalizing caller with
158+
| nil => rfl
159+
| cons c rest ih =>
160+
have hStep := external_call_preserves_caller_memory caller c.1 c.2.1 c.2.2
161+
regionLo regionHi (hAllDisj c (List.mem_cons_self ..)) i hLo hHi
162+
have hRest : ∀ d ∈ rest, d.1 + d.2.1 ≤ regionLo ∨ regionHi ≤ d.1 := by
163+
intro d hd; exact hAllDisj d (List.mem_cons_of_mem _ hd)
164+
have hIH := ih (applyCallToCaller caller c.1 c.2.1 c.2.2) hRest
165+
simp [List.foldl]; rw [hIH, hStep]
166+
167+
end Verity.EVM.Frame

0 commit comments

Comments
 (0)