Skip to content

Commit 2e99849

Browse files
authored
feat(EVM,Trace,Core): land ERC-4337 frame primitives upstream
Includes the path-resolution fix for #fromSolidity reported during review.
1 parent eff7310 commit 2e99849

11 files changed

Lines changed: 757 additions & 1 deletion

File tree

AXIOMS.md

Lines changed: 23 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 |

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/Compiler/FromSolidity.lean

Lines changed: 142 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,142 @@
1+
import Lean
2+
3+
namespace Verity.Compiler.FromSolidity
4+
5+
open Lean Elab Command
6+
7+
/-!
8+
# `#fromSolidity` Lean entry point — scaffold
9+
10+
Today the Verity compiler runs out-of-band via the `verity-cli`
11+
command-line tool: a developer manually invokes
12+
`verity-cli compile path/to/Foo.sol --emit lean` and copies the
13+
generated Lean term into the benchmark.
14+
15+
This module is the **scaffold** for surfacing that pipeline as a Lean
16+
elaborator-level command, so that a benchmark file can declare:
17+
18+
```
19+
#fromSolidity "vendor/account-abstraction/EntryPoint.sol"
20+
```
21+
22+
and obtain a proof-checked `verity_contract` term as if it had been
23+
hand-written.
24+
25+
## Status
26+
27+
This scaffold lands the public API surface (`#fromSolidity` command,
28+
options, error type) and a stub elaborator that defers to the CLI via
29+
`IO.Process.run`. The full machine translation requires hooking
30+
`Compiler/CompilationModel.lean`'s Solidity-IR pipeline directly into
31+
Lean's elaborator, which is a multi-week project tracked separately
32+
(see `docs/ROADMAP.md` "machine fromSolidity").
33+
34+
The scaffold exists so:
35+
36+
* The API contract is fixed (the command name and option shape will not
37+
change underneath downstream benchmarks).
38+
* The CLI integration point is documented as a single function
39+
(`runVerityCliCompile`) that the next PR replaces with an in-process
40+
call.
41+
* Benchmarks that today write `verity_contract` blocks by hand can
42+
switch to `#fromSolidity` once the CLI shells out cleanly, without
43+
changing their proof files.
44+
45+
## Trust boundary today vs. after full implementation
46+
47+
Today (`fromSolidity` shells out to `verity-cli`):
48+
* Trust the CLI binary on disk.
49+
* Trust the file-system path is consistent with what the developer
50+
proved against.
51+
52+
After full implementation (in-process):
53+
* No CLI binary trust — translation runs inside Lean.
54+
* The same kernel checks the resulting term that checks every Lean
55+
proof.
56+
57+
The headline difference: the post-implementation form makes
58+
*translation faithfulness* part of the proof-checked trust base.
59+
-/
60+
61+
/-- Options for `#fromSolidity`. -/
62+
structure Options where
63+
/-- Path to the Solidity source. Resolved relative to the workspace root. -/
64+
path : System.FilePath
65+
/-- Optional contract name selector (defaults to the only contract in the file). -/
66+
contractName : Option String := none
67+
/-- Verity CLI binary to invoke. Defaults to `verity-cli` on PATH. -/
68+
cliBinary : String := "verity-cli"
69+
deriving Repr
70+
71+
/-- Result of running the CLI translation step. -/
72+
inductive Result where
73+
| ok (lean : String)
74+
| error (msg : String)
75+
deriving Repr
76+
77+
/-- Locate the Lake workspace root containing the current process, if any. -/
78+
partial def findWorkspaceRoot? (dir : System.FilePath) : IO (Option System.FilePath) := do
79+
if ← (dir / "lakefile.lean").pathExists then
80+
pure (some dir)
81+
else if ← (dir / "lakefile.toml").pathExists then
82+
pure (some dir)
83+
else
84+
match dir.parent with
85+
| some parent =>
86+
if parent == dir then
87+
pure none
88+
else
89+
findWorkspaceRoot? parent
90+
| none => pure none
91+
92+
/-- Resolve `path` according to the `Options.path` contract.
93+
94+
Absolute paths are preserved. Relative paths are anchored at the Lake workspace
95+
root containing the current process, falling back to the process working
96+
directory when the command is run outside a Lake workspace. -/
97+
def resolveSourcePath (path : System.FilePath) : IO System.FilePath := do
98+
if path.isAbsolute then
99+
pure path.normalize
100+
else
101+
let cwd ← IO.currentDir
102+
let root ← findWorkspaceRoot? cwd
103+
pure (((root.getD cwd) / path).normalize)
104+
105+
/-- Invoke `verity-cli compile path --emit lean`. Returns the emitted
106+
Lean source on stdout or an error message. -/
107+
def runVerityCliCompile (opts : Options) : IO Result := do
108+
let sourcePath ← resolveSourcePath opts.path
109+
let args := match opts.contractName with
110+
| some c =>
111+
#["compile", sourcePath.toString, "--contract", c, "--emit", "lean"]
112+
| none =>
113+
#["compile", sourcePath.toString, "--emit", "lean"]
114+
try
115+
let out ← IO.Process.run { cmd := opts.cliBinary, args }
116+
return .ok out
117+
catch e =>
118+
return .error s!"verity-cli failed: {e.toString}"
119+
120+
/-- `#fromSolidity "path"` command. Currently emits a placeholder
121+
`axiom` whose name documents the pending translation; replace with a
122+
proper elaborator emission once the in-process translator lands. -/
123+
syntax (name := fromSolidityCmd) "#fromSolidity " str (" contract " str)? : command
124+
125+
@[command_elab fromSolidityCmd]
126+
def elabFromSolidity : CommandElab := fun stx => do
127+
let pathLit := stx[1]
128+
let path : System.FilePath := pathLit.isStrLit?.get!
129+
let contractOpt : Option String :=
130+
if stx[2].getNumArgs > 0 then some stx[2][1].isStrLit?.get! else none
131+
let opts : Options := { path := path, contractName := contractOpt }
132+
let result : Result ← (runVerityCliCompile opts).toIO
133+
(fun e => IO.userError s!"fromSolidity IO failure: {e.toString}")
134+
match result with
135+
| .ok _emitted =>
136+
let sourcePath ← (resolveSourcePath opts.path).toIO
137+
(fun e => IO.userError s!"fromSolidity path resolution failure: {e.toString}")
138+
logInfo m!"fromSolidity scaffold: would elaborate {sourcePath}; in-process translator pending"
139+
| .error msg =>
140+
throwError m!"fromSolidity: {msg}. Falling back: declare the verity_contract block by hand for now."
141+
142+
end Verity.Compiler.FromSolidity

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) =

0 commit comments

Comments
 (0)