From 541c3a8b998b2c605d71d2e99ccf8f6abbdee991 Mon Sep 17 00:00:00 2001 From: Thomas Marchand Date: Tue, 22 Sep 2026 18:41:03 +0100 Subject: [PATCH 01/22] Observe multi-value returns on the denotation stop state. Stmt.returnValues records source-order words and finishes as stop, so slice proofs can read the three results without a new statement outcome. Co-authored-by: Cursor --- .../Proofs/IRGeneration/DenoteAgreement.lean | 6 +- .../IRGeneration/DenoteEquivalence.lean | 2 +- .../IRGeneration/DenoteFunctionAgreement.lean | 5 +- .../Proofs/IRGeneration/SourceSemantics.lean | 67 ++++++++++++++++++- Verity/Core/Model/Denote.lean | 57 ++++++++++++++-- 5 files changed, 126 insertions(+), 11 deletions(-) diff --git a/Compiler/Proofs/IRGeneration/DenoteAgreement.lean b/Compiler/Proofs/IRGeneration/DenoteAgreement.lean index 9bb60c02b1..a453009713 100644 --- a/Compiler/Proofs/IRGeneration/DenoteAgreement.lean +++ b/Compiler/Proofs/IRGeneration/DenoteAgreement.lean @@ -36,7 +36,8 @@ def toRuntimeState (s : DenoteState) : SourceSemantics.RuntimeState := { world := s.world, immutable := s.immutable, bindings := s.bindings, selector := s.selector, externalCallOracle := fun n => ⟨s.externalCallSucceeded n, s.externalCallReturnValues n, s.externalCallPostWorld n⟩, - externalCallIndex := s.externalCallIndex } + externalCallIndex := s.externalCallIndex, + observedReturnWords := s.observedReturnWords } @[simp] theorem toRuntimeState_world (s : DenoteState) : (toRuntimeState s).world = s.world := rfl @@ -672,7 +673,8 @@ theorem execStmt_eq (fields : List Field) : · simp [toStmtResult, toRuntimeState, h, harity, hw, bindValues_eq, SourceSemantics.returndataAfterCall] · simp [toStmtResult, toRuntimeState, h] - | _, .returnValues .. | _, .returnArray .. | _, .returnBytes .. | _, .returnStorageWords .. + | _, .returnValues _ => by denote_stmt_arm + | _, .returnArray .. | _, .returnBytes .. | _, .returnStorageWords .. | _, .returnCodeData .. | _, .revertReturndata .. | _, .internalCall .. | _, .internalCallAssign .. | _, .rawLog .. | _, .unsafeBlock .. | _, .unsafeYul .. | _, .matchAdt .. => rfl diff --git a/Compiler/Proofs/IRGeneration/DenoteEquivalence.lean b/Compiler/Proofs/IRGeneration/DenoteEquivalence.lean index 725477a348..3d85185be2 100644 --- a/Compiler/Proofs/IRGeneration/DenoteEquivalence.lean +++ b/Compiler/Proofs/IRGeneration/DenoteEquivalence.lean @@ -99,7 +99,7 @@ theorem successResult_eq (spec : CompilationModel) (world : Verity.ContractState (ret : Option Nat) : toSourceResult (Denote.successResult sourceOracle spec world ret) = SourceSemantics.successResult spec world ret := - DenoteAgreement.toSourceResult_successResult spec world ret + DenoteAgreement.toSourceResult_successResult spec world ret [] theorem withTransactionContext_eq (world : Verity.ContractState) (tx : DenoteTransaction) : Denote.withTransactionContext world tx = diff --git a/Compiler/Proofs/IRGeneration/DenoteFunctionAgreement.lean b/Compiler/Proofs/IRGeneration/DenoteFunctionAgreement.lean index 8f34170deb..6090fe8865 100644 --- a/Compiler/Proofs/IRGeneration/DenoteFunctionAgreement.lean +++ b/Compiler/Proofs/IRGeneration/DenoteFunctionAgreement.lean @@ -179,8 +179,9 @@ theorem findDynamicArrayElementAtSlot_go_eq simp [toSourceResult, Denote.revertedResult, SourceSemantics.revertedResult] @[simp] theorem toSourceResult_successResult - (spec : CompilationModel) (world : Verity.ContractState) (ret : Option Nat) : - toSourceResult (Denote.successResult sourceOracle spec world ret) = + (spec : CompilationModel) (world : Verity.ContractState) (ret : Option Nat) + (returnWords : List Nat) : + toSourceResult (Denote.successResult sourceOracle spec world ret returnWords) = SourceSemantics.successResult spec world ret := by simp [toSourceResult, Denote.successResult, SourceSemantics.successResult] diff --git a/Compiler/Proofs/IRGeneration/SourceSemantics.lean b/Compiler/Proofs/IRGeneration/SourceSemantics.lean index 713dae8670..bbff2c0184 100644 --- a/Compiler/Proofs/IRGeneration/SourceSemantics.lean +++ b/Compiler/Proofs/IRGeneration/SourceSemantics.lean @@ -850,6 +850,10 @@ structure RuntimeState where selector : Nat := 0 externalCallOracle : Nat → ExternalCallOutcome := fun _ => ⟨false, [], none⟩ externalCallIndex : Nat := 0 + /-- Words produced by `Stmt.returnValues`, in source order. + Mirrors `DenoteState.observedReturnWords`. Proof semantics observes the + words, not a Solidity panic payload: `Stmt.panic` remains a revert. -/ + observedReturnWords : Option (List Nat) := none inductive StmtResult where | continue (state : RuntimeState) @@ -1189,6 +1193,22 @@ def evalExpr (fields : List Field) (state : RuntimeState) : Expr → Option Nat | .paramDynamicMemberDataOffset _ _ => none | .paramDynamicMemberElement _ _ _ => none | .paramDynamicStaticComposite _ _ => none + | .paramDynamicHeadWord name wordOffset => + -- Mirrors `Denote.evalExpr`: calldata variant of + -- `Compiler.CompilationModel.DynamicData.checkedParamDynamicHeadWordHelper`. + -- Reads the word at `data_offset + wordOffset * 32` and fails when the + -- position would read past `calldatasize() - 32` (modular, as in Yul). + match lookupBinding? state.bindings s!"{name}_data_offset" with + | some dataOffset => + let wordPos := (dataOffset + wordOffset * 32) % Compiler.Constants.evmModulus + let bound := + (state.world.calldataSize.val + Compiler.Constants.evmModulus - 32) % + Compiler.Constants.evmModulus + if wordPos > bound then none + else + some (Compiler.Proofs.YulGeneration.calldataloadWord + state.selector state.world.calldata wordPos) + | none => none | .literal n => some (wordNormalize n) | .param name => some (lookupValue state.bindings name) | .immutable name => some (state.immutable name).val @@ -1791,7 +1811,18 @@ private theorem evalExpr_paramDynamicHeadWord (state : RuntimeState) (name : String) (wordOffset : Nat) : - evalExpr fields state (.paramDynamicHeadWord name wordOffset) = none := rfl + evalExpr fields state (.paramDynamicHeadWord name wordOffset) = + match lookupBinding? state.bindings s!"{name}_data_offset" with + | some dataOffset => + let wordPos := (dataOffset + wordOffset * 32) % Compiler.Constants.evmModulus + let bound := + (state.world.calldataSize.val + Compiler.Constants.evmModulus - 32) % + Compiler.Constants.evmModulus + if wordPos > bound then none + else + some (Compiler.Proofs.YulGeneration.calldataloadWord + state.selector state.world.calldata wordPos) + | none => none := rfl private theorem evalExpr_paramDynamicStaticComposite (fields : List Field) @@ -2822,6 +2853,11 @@ mutual externalCallIndex := state.externalCallIndex + 1 } else .revert | none => .revert + | state, .returnValues values => + match evalExprList fields state values with + | some resolved => + .stop { state with observedReturnWords := some (resolved.map wordNormalize) } + | none => .revert | _, _ => .revert def execStmtListWithEvents (fields : List Field) (events : List EventDef) : @@ -3200,6 +3236,11 @@ mutual externalCallIndex := state.externalCallIndex + 1 } else .revert | none => .revert + | state, .returnValues values => + match evalExprList fields state values with + | some resolved => + .stop { state with observedReturnWords := some (resolved.map wordNormalize) } + | none => .revert | _, _ => .revert def execStmtList (fields : List Field) : RuntimeState → List Stmt → StmtResult @@ -4398,8 +4439,22 @@ mutual (Verity.Core.Uint256.ofNat baseVal) (Verity.Core.Uint256.ofNat exponentVal)).val else none + | .paramDynamicHeadWord name wordOffset => + -- Same semantics as `evalExpr` (no helper involvement): calldata + -- variant of `checkedParamDynamicHeadWordHelper`. + match lookupBinding? state.bindings s!"{name}_data_offset" with + | some dataOffset => + let wordPos := (dataOffset + wordOffset * 32) % Compiler.Constants.evmModulus + let bound := + (state.world.calldataSize.val + Compiler.Constants.evmModulus - 32) % + Compiler.Constants.evmModulus + if wordPos > bound then none + else + some (Compiler.Proofs.YulGeneration.calldataloadWord + state.selector state.world.calldata wordPos) + | none => none | .mulDiv512Down _ _ _ | .mulDiv512Up _ _ _ - | .paramDynamicHeadWord _ _ | .paramDynamicStaticComposite _ _ + | .paramDynamicStaticComposite _ _ | .paramDynamicMemberLength _ _ | .paramDynamicMemberDataOffset _ _ | .paramDynamicMemberElement _ _ _ | .arrayElementWord _ _ _ _ @@ -4826,6 +4881,14 @@ mutual externalCallIndex := state.externalCallIndex + 1 } else .revert | none => .revert + | .returnValues values => + -- Base expression semantics, matching `execStmt`. Arguments are not + -- given helper fuel; the slice importer inlines pure helpers before + -- this statement is produced. + match evalExprList fields state values with + | some resolved => + .stop { state with observedReturnWords := some (resolved.map wordNormalize) } + | none => .revert | _ => .revert termination_by stmt => (fuel, sizeOf stmt) decreasing_by all_goals (simp_wf; omega) diff --git a/Verity/Core/Model/Denote.lean b/Verity/Core/Model/Denote.lean index 9e65068978..48ae69fc63 100644 --- a/Verity/Core/Model/Denote.lean +++ b/Verity/Core/Model/Denote.lean @@ -58,8 +58,15 @@ semantic definition. ## Outside the initial denotation fragment Exactly the constructs `SourceSemantics` itself maps to `none`/`.revert`, apart -from the memory-backed word arrays intentionally widened here: -`arrayElementDynamic*`, `paramDynamic*`, `forkIfAtLeast`, `mappingChain` +from the memory-backed word arrays intentionally widened here and two +extensions landed with the Solidity-slice importer: +`Expr.paramDynamicHeadWord` (dynamic-tuple head-word projection, mirroring the +calldata variant of `checkedParamDynamicHeadWordHelper`) and +`Stmt.returnValues` (multi-word return recorded in +`DenoteState.observedReturnWords`, finished with `.stop`, and surfaced by +`denoteFunction` as `DenoteResult.returnWords` in source order). +Still outside: `arrayElementDynamic*`, the remaining `paramDynamic*`, +`forkIfAtLeast`, `mappingChain` reads with zero or three-plus keys (one/two-key reads and writes are supported), and every `Expr`/`Stmt` constructor not listed in the arms below (raw calls, ABI re-encoding returns, ECM, unsafe Yul, @@ -265,6 +272,12 @@ structure DenoteState where externalCallReturnValues : Nat → List Nat := fun _ => [] externalCallPostWorld : Nat → Option Verity.ContractState := fun _ => none externalCallIndex : Nat := 0 + /-- Words produced by `Stmt.returnValues`, in source order. + `none` means this frame has not executed a multi-value return. + The proof semantics records the words here and finishes with `.stop`; + it does not observe a Solidity panic payload. `Stmt.panic` is still a + revert, and the `PanicCode` stays on the statement for audit. -/ + observedReturnWords : Option (List Nat) := none /-- Mirrors `SourceSemantics.StmtResult`. -/ inductive StmtOutcome where @@ -746,6 +759,22 @@ def evalExpr (oracle : DenoteOracle) (fields : List Field) (state : DenoteState) | .paramDynamicMemberDataOffset _ _ => none | .paramDynamicMemberElement _ _ _ => none | .paramDynamicStaticComposite _ _ => none + | .paramDynamicHeadWord name wordOffset => + -- Mirrors the calldata variant of + -- `Compiler.CompilationModel.DynamicData.checkedParamDynamicHeadWordHelper`: + -- read the word at `data_offset + wordOffset * 32`, where `data_offset` + -- is the `{name}_data_offset` binding produced for a dynamic tuple + -- parameter, and fail when the position would read past + -- `calldatasize() - 32`. Arithmetic is modular, as in Yul. + match lookupBinding? state.bindings s!"{name}_data_offset" with + | some dataOffset => + let wordPos := (dataOffset + wordOffset * 32) % Compiler.Constants.evmModulus + let bound := + (state.world.calldataSize.val + Compiler.Constants.evmModulus - 32) % + Compiler.Constants.evmModulus + if wordPos > bound then none + else some (calldataloadWord state.selector state.world.calldata wordPos) + | none => none | .literal n => some (wordNormalize n) | .param name => some (lookupValue state.bindings name) | .immutable name => some (state.immutable name).val @@ -1515,6 +1544,11 @@ mutual else .revert | none => .revert | _, .revertReturndata => .revert + | state, .returnValues values => + match evalExprList oracle fields state values with + | some resolved => + .stop { state with observedReturnWords := some (resolved.map wordNormalize) } + | none => .revert | _, _ => .revert def execStmtList (oracle : DenoteOracle) (fields : List Field) : @@ -1548,6 +1582,10 @@ structure DenoteTransaction where structure DenoteResult where success : Bool returnValue : Option Nat + /-- Multi-word return observed from `Stmt.returnValues`. + Empty when the frame returned a single word or no word. + Order is the source order of the return expressions. -/ + returnWords : List Nat := [] finalStorage : Nat → Nat events : List (List Nat) @@ -1565,9 +1603,11 @@ def revertedResult (oracle : DenoteOracle) (spec : CompilationModel) events := encodeEvents initialWorld.events } def successResult (oracle : DenoteOracle) (spec : CompilationModel) - (world : Verity.ContractState) (ret : Option Nat) : DenoteResult := + (world : Verity.ContractState) (ret : Option Nat) (returnWords : List Nat := []) : + DenoteResult := { success := true returnValue := ret + returnWords := returnWords finalStorage := encodeStorage oracle spec world events := encodeEvents world.events } @@ -1653,7 +1693,16 @@ def denoteFunction (oracle : DenoteOracle) (spec : CompilationModel) (fn : Funct externalCallPostWorld := externalCallPostWorld } fn.body with | .continue state => successResult oracle spec state.world none - | .stop state => successResult oracle spec state.world none + | .stop state => + -- Multi-word returns (`Stmt.returnValues`) are observable in + -- `returnWords` (source order). `returnValue` stays `none` so that + -- `toSourceResult` — whose target type has no multi-word field — + -- still agrees with `SourceSemantics.interpretFunction`, which does + -- not surface `Stmt.returnValues` payloads. No `match` on + -- `observedReturnWords` here: keeping the arm match-free lets the + -- `denoteFunction_eq` agreement proof close by `simp`. + successResult oracle spec state.world none + (returnWords := state.observedReturnWords.getD []) | .return value state => successResult oracle spec state.world (some value) | .revert => revertedResult oracle spec worldWithTx From 4f3935d3241808c7cc7f672a390ea3b43f8ddb71 Mon Sep 17 00:00:00 2001 From: Thomas Marchand Date: Tue, 22 Sep 2026 18:41:10 +0100 Subject: [PATCH 02/22] Import one Solidity function into a covered CompilationModel. Select the function from pinned solc 0.8.34 before validating the slice, inline the reached helpers, and reject an uncovered constructor instead of executing it. Co-authored-by: Cursor --- AUDIT.md | 19 + AXIOMS.md | 9 + Compiler/SoliditySlice/Coverage.lean | 400 ++++++++ Compiler/SoliditySlice/Import.lean | 1221 +++++++++++++++++++++++ Compiler/SoliditySlice/Report.lean | 59 ++ Contracts/SoliditySliceSmoke/Lib.sol | 26 + Contracts/SoliditySliceSmoke/Slice.sol | 53 + Contracts/SoliditySliceSmoke/Smoke.lean | 98 ++ TRUST_ASSUMPTIONS.md | 28 + lakefile.lean | 3 + scripts/setup_solc_slice.py | 92 ++ 11 files changed, 2008 insertions(+) create mode 100644 Compiler/SoliditySlice/Coverage.lean create mode 100644 Compiler/SoliditySlice/Import.lean create mode 100644 Compiler/SoliditySlice/Report.lean create mode 100644 Contracts/SoliditySliceSmoke/Lib.sol create mode 100644 Contracts/SoliditySliceSmoke/Slice.sol create mode 100644 Contracts/SoliditySliceSmoke/Smoke.lean create mode 100644 scripts/setup_solc_slice.py diff --git a/AUDIT.md b/AUDIT.md index a7c1476ee2..1a6291a7e7 100644 --- a/AUDIT.md +++ b/AUDIT.md @@ -5,6 +5,25 @@ reviewable. Keep it synchronized with `TRUST_ASSUMPTIONS.md` and `AXIOMS.md` whenever semantics, trusted components, generated audit artifacts, or CI boundary checks change. +## Solidity function-slice import + +`Compiler/SoliditySlice` lowers one selected function and the callees solc's +declaration ids reach into a `CompilationModel`. `returnValues` is observable +on `DenoteState.observedReturnWords` and is mirrored in `SourceSemantics` and +`DenoteAgreement`. Accepted slices carry `sliceCovered`, proved by `decide` +against `modelSliceCovered`. The smoke contract +`Contracts/SoliditySliceSmoke` contains an unreached `for` loop; the import +still succeeds. `#eval` checks the interpreter result of two witnesses on that +imported body: `(95, 5, 5)` and a revert. `smoke_step_credit` is a kernel proof +of the first packed `credit` read. It is not a proof of the whole body, and it +is not a proof that the model matches solc bytecode. + +The slice compiler pin is solc `0.8.34+commit.80d5c536`, installed separately +from the Vault 0.8.33 pin by `python3 scripts/setup_solc_slice.py`. + +Evidence command: `lake build SoliditySliceSmoke` after the 0.8.34 compiler is +installed at `.lake/solidity-import/solc-0.8.34`. + ## Proof-only Solidity Vault POC The focused suite probes unknown, wrong-typed, and missing AST fields (including diff --git a/AXIOMS.md b/AXIOMS.md index d3454cfdc4..cd98efb193 100644 --- a/AXIOMS.md +++ b/AXIOMS.md @@ -2,6 +2,15 @@ This file is the authoritative registry of axioms used by Verity proof code. +## Solidity function-slice import + +`slice.sliceCovered` and `smoke_step_credit` are ordinary theorems. The slice +path adds no project axiom, `sorry`, or `native_decide`. `sliceCovered` is +`decide` on the whitelist. `smoke_step_credit` is `simp` plus `decide` on the +concrete packed read. Interpreter `#eval` checks are not theorems. The frontend that +builds the model from solc JSON remains a trust assumption; see +`TRUST_ASSUMPTIONS.md`. + ## Proof-only Solidity Vault audit `PrintAxioms.lean` includes the imported Vault execution theorems. The focused diff --git a/Compiler/SoliditySlice/Coverage.lean b/Compiler/SoliditySlice/Coverage.lean new file mode 100644 index 0000000000..cec6f85d4c --- /dev/null +++ b/Compiler/SoliditySlice/Coverage.lean @@ -0,0 +1,400 @@ +import Verity.Core.Model.Denote + +/-! +# Denotation coverage certificate for Solidity slices + +The Solidity-slice importer (`Compiler/SoliditySlice`) emits only a small, +explicitly listed fragment of the deep `CompilationModel`. This module is the +machine-checked side of that contract: + +* `exprCovered` / `stmtCovered` / `stmtListCovered` are the decidable + whitelists of `Expr` / `Stmt` constructors the importer may emit. A + constructor is whitelisted only when `Compiler.CompilationModel.Denote` + gives it a dedicated arm — never the catch-all `none` / `.revert` fallback. +* For every whitelisted constructor, an `*_arm` theorem pins the exact + denotation equation by `rfl`. If the corresponding `Denote` arm is removed + or its meaning changes, the pin stops compiling, so the whitelist cannot + silently drift away from the semantics. + +The importer refuses to register a slice whose body fails `stmtListCovered`, +and registers the passing verdict as a theorem proved by `decide`. + +This predicate is deliberately *not* `SupportedFunction`: that predicate is +the Denote↔IR raccord boundary (it rejects `panic`, `returnValues`, ...), +while this one characterizes the compiler-free `Denote` execution path only. +-/ + +namespace Compiler.CompilationModel.SoliditySlice + +open Compiler.CompilationModel +open Compiler.CompilationModel.Denote + +/-! ## Expression whitelist -/ + +/-- Expressions the slice importer may emit, mirrored against +`Denote.evalExpr`. Every `true` case has a dedicated arm there; every other +constructor (including `paramDynamicMember*`, `paramDynamicStaticComposite`, +`mulDiv512*`, raw calls, `internalCall`, `intrinsic`, `forkIfAtLeast`, ADTs, +`arrayElementWord`, `mappingChain`, and any future constructor) is rejected. -/ +def exprCovered : Expr → Bool + | .literal _ => true + | .param _ => true + | .localVar _ => true + | .blockTimestamp => true + | .structMember _ key _ => exprCovered key + | .structMember2 _ key1 key2 _ => exprCovered key1 && exprCovered key2 + | .add a b | .sub a b | .mul a b | .div a b + | .lt a b | .gt a b | .le a b | .ge a b | .eq a b + | .bitAnd a b | .bitXor a b => exprCovered a && exprCovered b + | .logicalNot a => exprCovered a + | _ => false + +/-- Every expression in the list is covered. -/ +def exprListCovered : List Expr → Bool + | [] => true + | e :: es => exprCovered e && exprListCovered es + +/-! ## Statement whitelist -/ + +mutual + +/-- Statements the slice importer may emit, mirrored against `Denote.execStmt`. +Multi-value returns are covered: `Stmt.returnValues` records the resolved words +in `DenoteState.observedReturnWords` and finishes with `.stop`. -/ +def stmtCovered : Stmt → Bool + | .letVar _ value => exprCovered value + | .assignVar _ value => exprCovered value + | .ite cond thenBranch elseBranch => + exprCovered cond && stmtListCovered thenBranch && stmtListCovered elseBranch + | .panic _ => true + | .returnValues args => exprListCovered args + | _ => false + +/-- Every statement in the list is covered. -/ +def stmtListCovered : List Stmt → Bool + | [] => true + | s :: ss => stmtCovered s && stmtListCovered ss + +end + +/-! ## Arm pins: expressions + +Each pin restates the dedicated `Denote.evalExpr` arm. They are proved by +`rfl`, so deleting the arm (falling back to the catch-all `none`) or changing +the arm's meaning breaks the build here. -/ + +theorem evalExpr_literal_arm (oracle : DenoteOracle) (fields : List Field) + (s : DenoteState) (n : Nat) : + evalExpr oracle fields s (.literal n) = some (wordNormalize n) := rfl + +theorem evalExpr_param_arm (oracle : DenoteOracle) (fields : List Field) + (s : DenoteState) (name : String) : + evalExpr oracle fields s (.param name) = some (lookupValue s.bindings name) := rfl + +theorem evalExpr_localVar_arm (oracle : DenoteOracle) (fields : List Field) + (s : DenoteState) (name : String) : + evalExpr oracle fields s (.localVar name) = some (lookupValue s.bindings name) := rfl + +theorem evalExpr_blockTimestamp_arm (oracle : DenoteOracle) (fields : List Field) + (s : DenoteState) : + evalExpr oracle fields s .blockTimestamp = some s.world.blockTimestamp.val := rfl + +theorem evalExpr_add_arm (oracle : DenoteOracle) (fields : List Field) + (s : DenoteState) (a b : Expr) : + evalExpr oracle fields s (.add a b) = + (do + let lhs : Verity.Core.Uint256 := ← evalExpr oracle fields s a + let rhs : Verity.Core.Uint256 := ← evalExpr oracle fields s b + pure (lhs + rhs).val) := rfl + +theorem evalExpr_sub_arm (oracle : DenoteOracle) (fields : List Field) + (s : DenoteState) (a b : Expr) : + evalExpr oracle fields s (.sub a b) = + (do + let lhs : Verity.Core.Uint256 := ← evalExpr oracle fields s a + let rhs : Verity.Core.Uint256 := ← evalExpr oracle fields s b + pure (lhs - rhs).val) := rfl + +theorem evalExpr_mul_arm (oracle : DenoteOracle) (fields : List Field) + (s : DenoteState) (a b : Expr) : + evalExpr oracle fields s (.mul a b) = + (do + let lhs : Verity.Core.Uint256 := ← evalExpr oracle fields s a + let rhs : Verity.Core.Uint256 := ← evalExpr oracle fields s b + pure (lhs * rhs).val) := rfl + +theorem evalExpr_div_arm (oracle : DenoteOracle) (fields : List Field) + (s : DenoteState) (a b : Expr) : + evalExpr oracle fields s (.div a b) = + (do + let lhs : Verity.Core.Uint256 := ← evalExpr oracle fields s a + let rhs : Verity.Core.Uint256 := ← evalExpr oracle fields s b + pure (lhs / rhs).val) := rfl + +theorem evalExpr_lt_arm (oracle : DenoteOracle) (fields : List Field) + (s : DenoteState) (a b : Expr) : + evalExpr oracle fields s (.lt a b) = + (do + let lhs ← evalExpr oracle fields s a + let rhs ← evalExpr oracle fields s b + pure (boolWord (decide (lhs < rhs)))) := rfl + +theorem evalExpr_gt_arm (oracle : DenoteOracle) (fields : List Field) + (s : DenoteState) (a b : Expr) : + evalExpr oracle fields s (.gt a b) = + (do + let lhs ← evalExpr oracle fields s a + let rhs ← evalExpr oracle fields s b + pure (boolWord (decide (rhs < lhs)))) := rfl + +theorem evalExpr_le_arm (oracle : DenoteOracle) (fields : List Field) + (s : DenoteState) (a b : Expr) : + evalExpr oracle fields s (.le a b) = + (do + let lhs ← evalExpr oracle fields s a + let rhs ← evalExpr oracle fields s b + pure (boolWord (decide (lhs ≤ rhs)))) := rfl + +theorem evalExpr_ge_arm (oracle : DenoteOracle) (fields : List Field) + (s : DenoteState) (a b : Expr) : + evalExpr oracle fields s (.ge a b) = + (do + let lhs ← evalExpr oracle fields s a + let rhs ← evalExpr oracle fields s b + pure (boolWord (decide (rhs ≤ lhs)))) := rfl + +theorem evalExpr_eq_arm (oracle : DenoteOracle) (fields : List Field) + (s : DenoteState) (a b : Expr) : + evalExpr oracle fields s (.eq a b) = + (do + let lhs ← evalExpr oracle fields s a + let rhs ← evalExpr oracle fields s b + pure (boolWord (decide (lhs = rhs)))) := rfl + +theorem evalExpr_bitAnd_arm (oracle : DenoteOracle) (fields : List Field) + (s : DenoteState) (a b : Expr) : + evalExpr oracle fields s (.bitAnd a b) = + (do + let lhs ← evalExpr oracle fields s a + let rhs ← evalExpr oracle fields s b + pure (Verity.Core.Uint256.and lhs rhs).val) := rfl + +theorem evalExpr_bitXor_arm (oracle : DenoteOracle) (fields : List Field) + (s : DenoteState) (a b : Expr) : + evalExpr oracle fields s (.bitXor a b) = + (do + let lhs ← evalExpr oracle fields s a + let rhs ← evalExpr oracle fields s b + pure (Verity.Core.Uint256.xor lhs rhs).val) := rfl + +/-- `Expr.logicalOr` and `Expr.logicalAnd` are eager in `evalExpr`, so they are +not slice constructors. `Expr.paramDynamicHeadWord` is an ABI-head read and is +excluded from this certificate; the slice does not claim public ABI decoding. -/ +theorem evalExpr_logicalNot_arm (oracle : DenoteOracle) (fields : List Field) + (s : DenoteState) (a : Expr) : + evalExpr oracle fields s (.logicalNot a) = + (do + let value ← evalExpr oracle fields s a + pure (boolWord (decide (value = 0)))) := rfl + +/-- Struct member reads through a single mapping key are denoted, including +packed sub-word slicing from the field's declared `StructMember` metadata. -/ +theorem evalExpr_structMember_arm (oracle : DenoteOracle) (fields : List Field) + (s : DenoteState) (field : String) (key : Expr) (memberName : String) : + evalExpr oracle fields s (.structMember field key memberName) = + (do + let keyVal ← evalExpr oracle fields s key + match findFieldWithResolvedSlot fields field, findStructMembers fields field with + | some (fieldInfo, slot), some members => + match findStructMember members memberName with + | some member => + let targetSlot := wordNormalize + (oracle.mappingSlot slot keyVal + member.wordOffset) + let rawWord := (readFieldWord s.world fieldInfo targetSlot).val + match member.packed with + | none => some rawWord + | some packed => + some (Verity.Core.Uint256.and + (Verity.Core.Uint256.shr packed.offset rawWord) + (packedMaskNat packed)).val + | none => none + | _, _ => none) := rfl + +/-- Struct member reads through two mapping keys are denoted. -/ +theorem evalExpr_structMember2_arm (oracle : DenoteOracle) (fields : List Field) + (s : DenoteState) (field : String) (key1 key2 : Expr) (memberName : String) : + evalExpr oracle fields s (.structMember2 field key1 key2 memberName) = + (do + let key1Val ← evalExpr oracle fields s key1 + let key2Val ← evalExpr oracle fields s key2 + match findFieldWithResolvedSlot fields field, findStructMembers fields field with + | some (fieldInfo, slot), some members => + match findStructMember members memberName with + | some member => + let innerSlot := oracle.mappingSlot slot key1Val + let outerSlot := oracle.mappingSlot innerSlot key2Val + let targetSlot := wordNormalize (outerSlot + member.wordOffset) + let rawWord := (readFieldWord s.world fieldInfo targetSlot).val + match member.packed with + | none => some rawWord + | some packed => + some (Verity.Core.Uint256.and + (Verity.Core.Uint256.shr packed.offset rawWord) + (packedMaskNat packed)).val + | none => none + | _, _ => none) := rfl + +/-! ## Arm pins: statements -/ + +theorem execStmt_letVar_arm (oracle : DenoteOracle) (fields : List Field) + (s : DenoteState) (name : String) (value : Expr) : + execStmt oracle fields s (.letVar name value) = + (match evalExpr oracle fields s value with + | some resolved => .continue { s with bindings := bindValue s.bindings name resolved } + | none => .revert) := rfl + +theorem execStmt_assignVar_arm (oracle : DenoteOracle) (fields : List Field) + (s : DenoteState) (name : String) (value : Expr) : + execStmt oracle fields s (.assignVar name value) = + (match evalExpr oracle fields s value with + | some resolved => .continue { s with bindings := bindValue s.bindings name resolved } + | none => .revert) := rfl + +/-- `Stmt.ite` is lazy in the denotation: only the taken branch executes, so +guards inside an untaken branch cannot introduce a spurious revert. -/ +theorem execStmt_ite_arm (oracle : DenoteOracle) (fields : List Field) + (s : DenoteState) (cond : Expr) (thenBranch elseBranch : List Stmt) : + execStmt oracle fields s (.ite cond thenBranch elseBranch) = + (match evalExpr oracle fields s cond with + | some condVal => + if condVal != 0 then + execStmtList oracle fields s thenBranch + else + execStmtList oracle fields s elseBranch + | none => .revert) := rfl + +theorem execStmt_panic_arm (oracle : DenoteOracle) (fields : List Field) + (s : DenoteState) (code : Verity.Core.PanicCode) : + execStmt oracle fields s (.panic code) = .revert := rfl + +/-- Multi-value returns are denoted: the resolved words are recorded in source +order in `observedReturnWords` and execution stops. -/ +theorem execStmt_returnValues_arm (oracle : DenoteOracle) (fields : List Field) + (s : DenoteState) (args : List Expr) : + execStmt oracle fields s (.returnValues args) = + (match evalExprList oracle fields s args with + | some resolved => + .stop { s with observedReturnWords := some (resolved.map wordNormalize) } + | none => .revert) := rfl + +/-- A `returnValues` statement terminates the statement list: later statements +(a `panic` here) never execute, and the recorded words survive in source +order. -/ +theorem execStmtList_returnValues_stop_arm (oracle : DenoteOracle) (fields : List Field) + (s : DenoteState) (a b : Nat) : + execStmtList oracle fields s + [.returnValues [.literal a, .literal b], .panic .arithmeticOverflow] = + .stop { s with observedReturnWords := some [wordNormalize a, wordNormalize b] } := by + simp [execStmtList, execStmt, evalExprList, evalExpr, wordNormalize] + +/-- Every function body of a model is in the slice whitelist. -/ +def modelSliceCovered (model : CompilationModel) : Bool := + model.functions.all fun fn => stmtListCovered fn.body + +/-- Level A: run a body on scalar bindings. Arguments are Solidity words, not +ABI-decoded calldata. `some` is the source-order return list of a successful +run; `none` is a revert. The panic payload is not returned. -/ +def denoteScalarBody + (oracle : DenoteOracle) (fields : List Field) + (world : Verity.ContractState) (timestamp : Nat) + (bindings : Env) (body : List Stmt) : Option (List Nat) := + match execStmtList oracle fields + { world := { world with blockTimestamp := Verity.Core.Uint256.ofNat timestamp } + bindings := bindings } body with + | .stop state | .continue state | .return _ state => state.observedReturnWords + | .revert => none + +private theorem boolAnd2 {a b : Bool} (h : (a && b) = true) : a = true ∧ b = true := by + cases a <;> cases b <;> simp_all + +private theorem boolAnd3 {a b c : Bool} (h : (a && b && c) = true) : + b = true ∧ c = true := by + cases a <;> cases b <;> cases c <;> simp_all + +/-- `next.world = origin.world` whenever execution continues, stops, or returns. +A revert has no successor state. -/ +def preservesWorld (origin : DenoteState) : StmtOutcome → Prop + | .continue next | .stop next | .return _ next => next.world = origin.world + | .revert => True + +mutual + +theorem execStmt_slice_world + (oracle : DenoteOracle) (fields : List Field) (state : DenoteState) (stmt : Stmt) + (h : stmtCovered stmt = true) : + preservesWorld state (execStmt oracle fields state stmt) := by + unfold stmtCovered at h + split at h + · rename_i name value + cases hEval : evalExpr oracle fields state value <;> + simp [preservesWorld, execStmt, hEval] + · rename_i name value + cases hEval : evalExpr oracle fields state value <;> + simp [preservesWorld, execStmt, hEval] + · rename_i cond thenBranch elseBranch + have ⟨hthen, helse⟩ := boolAnd3 h + cases hEval : evalExpr oracle fields state cond + · simp [preservesWorld, execStmt, hEval] + · rename_i bit + by_cases hbit : (bit != 0) = true + · simpa [preservesWorld, execStmt, hEval, hbit] using + execStmtList_slice_world oracle fields state thenBranch hthen + · have hnot : (bit != 0) = false := by simpa using hbit + simpa [preservesWorld, execStmt, hEval, hnot] using + execStmtList_slice_world oracle fields state elseBranch helse + · simp [preservesWorld, execStmt] + · rename_i args + cases hEval : evalExprList oracle fields state args <;> + simp [preservesWorld, execStmt, hEval] + · cases h + +theorem execStmtList_slice_world + (oracle : DenoteOracle) (fields : List Field) (state : DenoteState) + (stmts : List Stmt) (h : stmtListCovered stmts = true) : + preservesWorld state (execStmtList oracle fields state stmts) := by + match stmts with + | [] => simp [preservesWorld, execStmtList] + | stmt :: rest => + unfold stmtListCovered at h + have ⟨hstmt, hrest⟩ := boolAnd2 h + have hone := execStmt_slice_world oracle fields state stmt hstmt + simp only [execStmtList] + cases hExec : execStmt oracle fields state stmt + · rename_i next + have hworld : next.world = state.world := by + simpa [preservesWorld, hExec] using hone + have htail := execStmtList_slice_world oracle fields next rest hrest + simpa [preservesWorld, hworld] using htail + · rename_i next + simpa [preservesWorld, hExec] using hone + · rename_i value next + simpa [preservesWorld, hExec] using hone + · simp [preservesWorld] + +end + +/-- A covered slice does not write persistent storage. A revert leaves the +caller's pre-state in place because `denoteScalarBody` discards it. -/ +theorem execStmtList_slice_storageWords + (oracle : DenoteOracle) (fields : List Field) + (world : Verity.ContractState) (timestamp : Nat) + (bindings : Env) (body : List Stmt) + (h : stmtListCovered body = true) : + let state : DenoteState := + { world := { world with blockTimestamp := Verity.Core.Uint256.ofNat timestamp } + bindings := bindings } + preservesWorld state (execStmtList oracle fields state body) := by + intro state + exact execStmtList_slice_world oracle fields state body h + +end Compiler.CompilationModel.SoliditySlice diff --git a/Compiler/SoliditySlice/Import.lean b/Compiler/SoliditySlice/Import.lean new file mode 100644 index 0000000000..eda2a2cdfd --- /dev/null +++ b/Compiler/SoliditySlice/Import.lean @@ -0,0 +1,1221 @@ +import Lean +import Compiler.Sha256.Engine +import Compiler.SoliditySlice.Coverage +import Compiler.SoliditySlice.Report + +/-! +Slice importer. Pinned `solc --standard-json` supplies the AST, declaration +ids, and `storageLayout`. This module selects one function, closes over the +definitions that resolution actually reaches, and elaborates a +`CompilationModel`. It does not interpret that model: execution is +`Denote.execStmt`, restricted by `stmtListCovered`. + +The frontend is in the trust base. A covered model is not, by itself, a proof +that the model matches the Solidity source or solc's bytecode. +-/ + +open Lean Meta Elab Command + +namespace Compiler.CompilationModel.SoliditySlice + +def importerVersion : String := "solidity-slice-1" + +private def solcLongVersion := "0.8.34+commit.80d5c536" + +private def officialSolcSha256s : Array String := #[ + "d40adc6f9fdbb22a97d32a02fa05688bf2ee7886affc48c9851b0afd4a726b39", + "0a2829292697dda542e4e365bb63fbd6d3ed51537140222a880ab760cffa7746"] + +private def acceptedSolcBanners : Array String := #[ + s!"solc, the solidity compiler commandline interface\nVersion: {solcLongVersion}.Linux.g++", + s!"solc, the solidity compiler commandline interface\nVersion: {solcLongVersion}.Darwin.appleclang"] + +private def field (j : Json) (key : String) : MetaM Json := + match j.getObjVal? key with + | .ok v => pure v + | .error e => throwError "{e}" + +private def field? (j : Json) (key : String) : Option Json := + (j.getObjVal? key).toOption + +private def str (j : Json) : MetaM String := + match j.getStr? with + | .ok v => pure v + | .error e => throwError "{e}" + +private def arr (j : Json) : MetaM (Array Json) := + match j.getArr? with + | .ok v => pure v + | .error e => throwError "{e}" + +private def bool (j : Json) : MetaM Bool := + match j.getBool? with + | .ok v => pure v + | .error e => throwError "{e}" + +private def flexNat (j : Json) : MetaM Nat := do + match j.getNat? with + | .ok n => pure n + | .error _ => + match j.getStr? with + | .ok s => + match s.toNat? with + | some n => pure n + | none => throwError "not a nat: {s}" + | .error e => throwError "{e}" + +private def nodeKind (j : Json) : MetaM String := do + str (← field j "nodeType") + +private def typeString (j : Json) : MetaM String := do + str (← field (← field j "typeDescriptions") "typeString") + +private def optStr (j : Json) (key : String) : Option String := + match field? j key with + | some v => v.getStr?.toOption + | none => none + +private def hexDigit (n : Nat) : Char := + if n < 10 then Char.ofNat ('0'.toNat + n) else Char.ofNat ('a'.toNat + n - 10) + +private def sha256Hex (bytes : ByteArray) : String := + (Sha256Engine.sha256 bytes).data.foldl (init := "") fun acc byte => + acc.push (hexDigit (byte.toNat / 16)) |>.push (hexDigit (byte.toNat % 16)) + +private def leanStr (s : String) : String := + let escaped := s.foldl (init := "") fun acc c => + if c == '\"' || c == '\\' then acc ++ "\\" ++ c.toString else acc.push c + "\"" ++ escaped ++ "\"" + +private def leanList (xs : Array String) : String := + "[" ++ String.intercalate ", " xs.toList ++ "]" + +private def ex (body : String) : String := + s!"(Compiler.CompilationModel.Expr.{body})" + +private def st (body : String) : String := + s!"(Compiler.CompilationModel.Stmt.{body})" + +private structure Val where + pre : Array String + expr : String + +private inductive SPath where + | one (field key : String) + | two (field k1 k2 : String) + | outer (field k1 : String) + +private inductive Ref where + | expr (v : Val) + | path (pre : Array String) (p : SPath) + | mem (id : Nat) (pre : Array String) + | state (name : String) (pre : Array String) + +private structure MemParam where + param : String + structName : String + members : Array (String × String) + +private structure FieldInfo where + name : String + slot : Nat + term : String + memberNames : Array String + opaqueNames : Array String + +private structure FnRec where + contract : String + name : String + declId : Nat + paramTypes : Array String + +private structure ProjRec where + parameter : String + member : String + structName : String + headWord : Nat + modelParam : String + typeString : String + +private structure OpaqRec where + field : String + name : String + solcType : String + wordOffset : Nat + byteOffset : Nat + +private structure SrcParam where + name : String + id : Nat + typeString : String + location : String + +private structure Env where + nodes : RBMap Nat Json compare + nodeFile : RBMap Nat String compare + files : RBMap String ByteArray compare + funs : RBMap Nat Json compare + funContract : RBMap Nat String compare + structs : RBMap Nat Json compare + stateVars : RBMap Nat Json compare + values : RBMap Nat String compare + paths : RBMap Nat SPath compare + mems : RBMap Nat MemParam compare + fieldsByName : RBMap String FieldInfo compare + included : Array FnRec + projections : Array ProjRec + opaqueMembers : Array OpaqRec + referenced : Array String + scalarTy : RBMap Nat String compare + next : Nat + stack : List Nat + yulNames : RBMap String String compare + currentFile : String + +private def Env.init : Env where + nodes := RBMap.empty + nodeFile := RBMap.empty + files := RBMap.empty + funs := RBMap.empty + funContract := RBMap.empty + structs := RBMap.empty + stateVars := RBMap.empty + values := RBMap.empty + paths := RBMap.empty + mems := RBMap.empty + fieldsByName := RBMap.empty + included := #[] + projections := #[] + opaqueMembers := #[] + referenced := #[] + scalarTy := RBMap.empty + next := 0 + stack := [] + yulNames := RBMap.empty + currentFile := "" + +abbrev M := StateT Env MetaM + +private def mField (j : Json) (key : String) : M Json := liftM (field j key) +private def mStr (j : Json) : M String := liftM (str j) +private def mArr (j : Json) : M (Array Json) := liftM (arr j) +private def mBool (j : Json) : M Bool := liftM (bool j) +private def mNat (j : Json) : M Nat := liftM (flexNat j) +private def mKind (j : Json) : M String := liftM (nodeKind j) +private def mType (j : Json) : M String := liftM (typeString j) + +private def failAt (j : Json) (why : String) : M α := do + let env ← get + let file := + match (field? j "id").bind (fun id => id.getNat?.toOption) with + | some id => env.nodeFile.find? id |>.getD env.currentFile + | none => env.currentFile + let some bytes := env.files.find? file | throwError "{file}: {why}" + let some src := (field? j "src").bind (fun s => s.getStr?.toOption) + | throwError "{file}: {why}" + let pieces := src.splitOn ":" + let some start := pieces[0]?.bind (·.toNat?) | throwError "{file}: {why}" + let some size := pieces[1]?.bind (·.toNat?) | throwError "{file}: {why}" + let before := bytes.data.extract 0 (min start bytes.size) + let (line, column) := before.foldl + (fun (p : Nat × Nat) b => if b == 10 then (p.1 + 1, 1) else (p.1, p.2 + 1)) (1, 1) + let excerptBytes : ByteArray := + ⟨bytes.data.extract start (min (start + size) (min bytes.size (start + 120)))⟩ + let excerpt := String.fromUTF8? excerptBytes |>.getD "" + let kind := (field? j "nodeType").bind (fun k => k.getStr?.toOption) |>.getD "node" + throwError "{file}:{line}:{column}: {kind}: {why}\n{excerpt}" + +private def refInt (j : Json) : M Int := do + match field? j "overloadedDeclarations" with + | some over => + let xs ← mArr over + if xs.size ≠ 0 then failAt j "ambiguous declaration" + | none => pure () + match (← mField j "referencedDeclaration").getInt? with + | .ok i => pure i + | .error e => failAt j e + +private def fresh : M String := do + let n := (← get).next + modify fun e => { e with next := e.next + 1 } + pure s!"sliceTmp{n}" + +private def isAtom (expr : String) : Bool := + expr.startsWith "(Compiler.CompilationModel.Expr.literal " || + expr.startsWith "(Compiler.CompilationModel.Expr.localVar " || + expr.startsWith "(Compiler.CompilationModel.Expr.param " || + expr == "(Compiler.CompilationModel.Expr.blockTimestamp)" + +private def atom (v : Val) : M Val := do + if v.pre.isEmpty && isAtom v.expr then + pure v + else + let n ← fresh + pure { pre := v.pre.push (st s!"letVar {leanStr n} {v.expr}"), + expr := ex s!"localVar {leanStr n}" } + +private def bitsOf (ty : String) : Option Nat := + if ty == "uint256" || ty == "uint" then some 256 + else if ty == "bytes32" then some 256 + else if ty == "address" then some 160 + else if ty.startsWith "uint" then (ty.drop 4).toNat? + else none + +private def paramTypeSyntax (ty : String) : Option String := + match ty with + | "uint256" | "uint" => some "Compiler.CompilationModel.ParamType.uint256" + | "address" => some "Compiler.CompilationModel.ParamType.address" + | "bytes32" => some "Compiler.CompilationModel.ParamType.bytes32" + | "bool" => some "Compiler.CompilationModel.ParamType.bool" + | _ => + match bitsOf ty with + | some n => + if n != 256 && ty.startsWith "uint" then + some s!"(Compiler.CompilationModel.ParamType.uintN {n})" + else none + | none => none + +private def noteFn (fn : Json) : M Unit := do + let id ← mNat (← mField fn "id") + let env ← get + unless env.included.any (·.declId == id) do + let name ← mStr (← mField fn "name") + let params ← mArr (← mField (← mField fn "parameters") "parameters") + let mut tys : Array String := #[] + for p in params do + tys := tys.push (← mType p) + let contract := env.funContract.find? id |>.getD "" + modify fun e => + let added := e.included.push ⟨contract, name, id, tys⟩ + { e with included := added } + +private def markField (name : String) : M Unit := do + let env ← get + unless env.referenced.contains name do + modify fun e => { e with referenced := e.referenced.push name } + +private def noteProjection (p : ProjRec) : M Unit := do + let env ← get + unless env.projections.any (fun q => q.parameter == p.parameter && q.member == p.member) do + modify fun e => { e with projections := e.projections.push p } + +private def memberRead (pre : Array String) (path : SPath) (member : String) (at_ : Json) : M Val := do + let (fieldName, exprBody) ← match path with + | .one field key => pure (field, s!"structMember {leanStr field} {key} {leanStr member}") + | .two field k1 k2 => pure (field, s!"structMember2 {leanStr field} {k1} {k2} {leanStr member}") + | .outer _ _ => failAt at_ "member access on an incomplete mapping" + let some info := (← get).fieldsByName.find? fieldName + | failAt at_ s!"no storage layout for {fieldName}" + if info.opaqueNames.contains member then + failAt at_ s!"member {member} is opaque in this slice" + unless info.memberNames.contains member do + failAt at_ s!"member {member} is not a layout member of {fieldName}" + markField fieldName + pure { pre, expr := ex exprBody } + +private def bin (op lhs rhs : String) : String := + ex (op ++ " " ++ lhs ++ " " ++ rhs) + +private def lit (n : Nat) : String := + ex s!"literal {n}" + +private def localName (name : String) : String := + ex ("localVar " ++ leanStr name) + +private def letBind (name value : String) : String := + st ("letVar " ++ leanStr name ++ " " ++ value) + +private def iteStmt (cond thenStmt elseStmt : String) : String := + st ("ite " ++ cond ++ " [" ++ thenStmt ++ "] [" ++ elseStmt ++ "]") + +private def overflowPanic : String := + st "panic Verity.Core.PanicCode.arithmeticOverflow" + +private def divPanic : String := + st "panic Verity.Core.PanicCode.divisionByZero" + +mutual + +private partial def lowerYul (j : Json) : M String := do + match ← mKind j with + | "YulIdentifier" => + let name ← mStr (← mField j "name") + match (← get).yulNames.find? name with + | some expr => pure expr + | none => failAt j s!"unbound Yul identifier {name}" + | "YulFunctionCall" => + let fname ← mStr (← mField (← mField j "functionName") "name") + let args ← mArr (← mField j "arguments") + let mut xs : Array String := #[] + for arg in args do + xs := xs.push (← lowerYul arg) + match fname, xs with + | "xor", #[a, b] => pure (ex s!"bitXor {a} {b}") + | "mul", #[a, b] => pure (ex s!"mul {a} {b}") + | "lt", #[a, b] => pure (ex s!"lt {a} {b}") + | _, _ => failAt j s!"unsupported Yul builtin {fname}" + | kind => failAt j s!"unsupported Yul node {kind}" + +private partial def lowerExpr (j : Json) : M Val := do + match ← mKind j with + | "Literal" => + let raw := optStr j "value" |>.getD "" + let some n := raw.toNat? | failAt j s!"unsupported literal {raw}" + pure { pre := #[], expr := ex s!"literal {n}" } + | "TupleExpression" => + if ← mBool (← mField j "isInlineArray") then failAt j "inline arrays are outside this slice" + let cs ← mArr (← mField j "components") + if cs.size == 1 then + if cs[0]!.isNull then failAt j "empty tuple component" + lowerExpr cs[0]! + else + failAt j "a multi-value expression is only valid as a return" + | "BinaryOperation" => lowerBinary j + | "Conditional" => lowerConditional j + | "FunctionCall" => lowerCall j + | "Identifier" | "MemberAccess" | "IndexAccess" => + match ← lowerRef j with + | .expr v => pure v + | _ => failAt j "storage or memory path used as a value" + | kind => failAt j s!"unsupported expression {kind}" + +private partial def lowerRef (j : Json) : M Ref := do + match ← mKind j with + | "Identifier" => + let id ← refInt j + if id < 0 then failAt j "unresolved builtin identifier" + let n := id.toNat + let env ← get + if let some expr := env.values.find? n then + pure (.expr { pre := #[], expr }) + else if let some path := env.paths.find? n then + pure (.path #[] path) + else if env.mems.contains n then + pure (.mem n #[]) + else if let some decl := env.stateVars.find? n then + pure (.state (← mStr (← mField decl "name")) #[]) + else + failAt j s!"unresolved identifier {id}" + | "IndexAccess" => + let base ← lowerRef (← mField j "baseExpression") + let key ← atom (← lowerExpr (← mField j "indexExpression")) + match base with + | .state name pre => + let some info := (← get).fieldsByName.find? name + | failAt j s!"no storage layout for {name}" + markField name + if info.term.contains "mappingStruct2" then + pure (.path (pre ++ key.pre) (.outer name key.expr)) + else + pure (.path (pre ++ key.pre) (.one name key.expr)) + | .path pre (.outer field k1) => + pure (.path (pre ++ key.pre) (.two field k1 key.expr)) + | _ => failAt j "index of a non-mapping" + | "MemberAccess" => + let member ← mStr (← mField j "memberName") + let base ← mField j "expression" + if member == "timestamp" && (← mKind base) == "Identifier" && + optStr base "name" == some "block" then + pure (.expr { pre := #[], expr := ex "blockTimestamp" }) + else if member == "max" then + lowerTypeMax j base + else + match ← lowerRef base with + | .path pre path => + pure (.expr (← memberRead pre path member j)) + | .mem id pre => + pure (.expr (← projectMember id pre member j)) + | _ => failAt j s!"unsupported member {member}" + | kind => failAt j s!"unsupported reference {kind}" + +private partial def lowerTypeMax (at_ base : Json) : M Ref := do + unless (← mKind base) == "FunctionCall" do + failAt at_ "max is only supported on type(T)" + let callee ← mField base "expression" + unless (← mKind callee) == "Identifier" && optStr callee "name" == some "type" do + failAt at_ "max is only supported on type(T)" + let args ← mArr (← mField base "arguments") + unless args.size == 1 do failAt at_ "type() expects one argument" + let arg := args[0]! + unless (← mKind arg) == "ElementaryTypeNameExpression" do + failAt at_ "type() expects an elementary type" + let tname ← mStr (← mField (← mField arg "typeName") "name") + let some bits := bitsOf tname | failAt at_ s!"unsupported type().max {tname}" + pure (.expr { pre := #[], expr := ex s!"literal {2 ^ bits - 1}" }) + +private partial def projectMember (id : Nat) (pre : Array String) (member : String) (at_ : Json) : M Val := do + let some mem := (← get).mems.find? id | failAt at_ "unknown memory parameter" + let some idx := mem.members.findIdx? (fun p => p.1 == member) + | failAt at_ s!"{member} is not a member of {mem.structName}" + let mut head : Nat := 0 + for i in [:idx] do + let ty := mem.members[i]!.2 + if ty.endsWith "[]" then + head := head + 1 + else if ty.startsWith "struct " || ty.any (· == '[') then + failAt at_ s!"cannot place {member}; earlier member {mem.members[i]!.1} is {ty}" + else + head := head + 1 + let modelParam := s!"{mem.param}_{member}" + noteProjection { + parameter := mem.param, member := member, structName := mem.structName, + headWord := head, modelParam := modelParam, typeString := mem.members[idx]!.2 } + pure { pre, expr := ex s!"param {leanStr modelParam}" } + +private partial def lowerBinary (j : Json) : M Val := do + let op ← mStr (← mField j "operator") + let left ← lowerExpr (← mField j "leftExpression") + let right ← lowerExpr (← mField j "rightExpression") + let common ← mStr (← mField (← mField j "commonType") "typeString") + match op with + | "+" => + let some bits := bitsOf common | failAt j s!"unsupported add type {common}" + checkedAdd bits left right + | "-" => checkedSub left right + | "*" => + let some bits := bitsOf common | failAt j s!"unsupported mul type {common}" + checkedMul bits left right + | "/" => checkedDiv left right + | "<" => cmp "lt" left right + | ">" => cmp "gt" left right + | "<=" => cmp "le" left right + | ">=" => cmp "ge" left right + | "==" => cmp "eq" left right + | "!=" => + let v ← cmp "eq" left right + pure { v with expr := ex s!"logicalNot {v.expr}" } + | _ => failAt j s!"unsupported operator {op}" + +private partial def checkedSub (left right : Val) : M Val := do + let a ← atom left + let b ← atom right + let dest ← fresh + let ok := letBind dest (bin "sub" a.expr b.expr) + let ite := iteStmt (bin "lt" a.expr b.expr) overflowPanic ok + pure { pre := a.pre ++ b.pre |>.push ite, expr := localName dest } + +private partial def checkedDiv (left right : Val) : M Val := do + let a ← atom left + let b ← atom right + let dest ← fresh + let ok := letBind dest (bin "div" a.expr b.expr) + let ite := iteStmt (bin "eq" b.expr (lit 0)) divPanic ok + pure { pre := a.pre ++ b.pre |>.push ite, expr := localName dest } + +private partial def checkedAdd (bits : Nat) (left right : Val) : M Val := do + let a ← atom left + let b ← atom right + let dest ← fresh + let sum := bin "add" a.expr b.expr + let ok := letBind dest sum + let cond := + if bits == 256 then bin "lt" sum a.expr + else bin "lt" (lit (2 ^ bits - 1)) sum + pure { pre := a.pre ++ b.pre |>.push (iteStmt cond overflowPanic ok), expr := localName dest } + +private partial def checkedMul (bits : Nat) (left right : Val) : M Val := do + let a ← atom left + let b ← atom right + let dest ← fresh + let prod := bin "mul" a.expr b.expr + let ok := letBind dest prod + if bits == 256 then + let inner := iteStmt (bin "eq" (bin "div" prod a.expr) b.expr) ok overflowPanic + let ite := iteStmt (bin "eq" a.expr (lit 0)) ok inner + pure { pre := a.pre ++ b.pre |>.push ite, expr := localName dest } + else + let cond := bin "lt" (lit (2 ^ bits - 1)) prod + pure { pre := a.pre ++ b.pre |>.push (iteStmt cond overflowPanic ok), expr := localName dest } + +private partial def cmp (op : String) (left right : Val) : M Val := do + let a ← atom left + let b ← atom right + pure { pre := a.pre ++ b.pre, expr := ex s!"{op} {a.expr} {b.expr}" } + +private partial def lowerConditional (j : Json) : M Val := do + let cond ← atom (← lowerExpr (← mField j "condition")) + let yes ← lowerExpr (← mField j "trueExpression") + let no ← lowerExpr (← mField j "falseExpression") + let dest ← fresh + let thenB := yes.pre.push (st s!"letVar {leanStr dest} {yes.expr}") + let elseB := no.pre.push (st s!"letVar {leanStr dest} {no.expr}") + let ite := st s!"ite {cond.expr} {leanList thenB} {leanList elseB}" + pure { pre := cond.pre.push ite, expr := ex s!"localVar {leanStr dest}" } + +private partial def lowerCast (j : Json) : M Val := do + let targetExpr ← mField j "expression" + unless (← mKind targetExpr) == "ElementaryTypeNameExpression" do + failAt j "unsupported cast" + let tname ← mStr (← mField (← mField targetExpr "typeName") "name") + let some bits := bitsOf tname | failAt j s!"unsupported cast target {tname}" + let args ← mArr (← mField j "arguments") + unless args.size == 1 do failAt j "cast expects one argument" + let v ← lowerExpr args[0]! + let src := ← mType args[0]! + let narrow : Bool := + match bitsOf src with + | some sb => decide (sb > bits) + | none => decide (bits < 256) && !src.startsWith "int_const" + if narrow then + let a ← atom v + let dest ← fresh + let bound := letBind dest (bin "bitAnd" a.expr (lit (2 ^ bits - 1))) + pure { pre := a.pre.push bound, expr := localName dest } + else + pure v + +private partial def lowerCall (j : Json) : M Val := do + let kind ← mStr (← mField j "kind") + if kind == "typeConversion" then + lowerCast j + else if kind == "functionCall" then + let callee ← mField j "expression" + let (fnId, receiver?) ← match ← mKind callee with + | "MemberAccess" => + let id ← refInt callee + if id < 0 then failAt callee "builtin call is outside this slice" + let base ← mField callee "expression" + let baseTy ← mType base + if baseTy.startsWith "type(library " then + pure (id.toNat, none) + else + pure (id.toNat, some base) + | "Identifier" => + let id ← refInt callee + if id < 0 then failAt callee "builtin call is outside this slice" + pure (id.toNat, none) + | _ => failAt callee "unsupported callee" + let args ← mArr (← mField j "arguments") + let mut vals : Array Val := #[] + if let some recv := receiver? then + vals := vals.push (← lowerExpr recv) + for arg in args do + vals := vals.push (← lowerExpr arg) + inlineFn fnId vals j + else + failAt j s!"unsupported call kind {kind}" + +private partial def inlineFn (fnId : Nat) (args : Array Val) (at_ : Json) : M Val := do + if (← get).stack.contains fnId then failAt at_ s!"recursive call {fnId}" + let some fn := (← get).funs.find? fnId | failAt at_ s!"unresolved function {fnId}" + let mods ← mArr (← mField fn "modifiers") + unless mods.isEmpty do failAt fn "modifiers are outside this slice" + let params ← mArr (← mField (← mField fn "parameters") "parameters") + unless params.size == args.size do + failAt at_ s!"call arity {args.size} does not match declaration {params.size}" + let rets ← mArr (← mField (← mField fn "returnParameters") "parameters") + unless rets.size == 1 do failAt fn "only single-value helpers are inlined" + let retName ← mStr (← mField rets[0]! "name") + let savedYul := (← get).yulNames + let savedFile := (← get).currentFile + let frameFile := (← get).nodeFile.find? fnId |>.getD (← get).currentFile + let frame := fnId :: (← get).stack + modify fun e => { e with stack := frame, currentFile := frameFile } + let mut pre : Array String := #[] + let mut yul := savedYul + for i in [:params.size] do + let p := params[i]! + let pname ← mStr (← mField p "name") + let pid ← mNat (← mField p "id") + let pty ← mType p + let argNode ← argumentAt at_ i + let argTy ← mType argNode + let some arg := args[i]? | failAt at_ s!"missing argument {i}" + let converted ← convert pty argTy arg at_ + let bound ← atom converted + pre := pre ++ bound.pre + modify fun e => { e with values := e.values.insert pid bound.expr } + if pname != "" then + yul := yul.insert pname bound.expr + modify fun e => { e with yulNames := yul } + noteFn fn + let body ← mField fn "body" + let stmts ← mArr (← mField body "statements") + let result ← lowerHelper stmts retName + modify fun e => { e with stack := e.stack.tail!, yulNames := savedYul, currentFile := savedFile } + pure { pre := pre ++ result.pre, expr := result.expr } + +private partial def argumentAt (call : Json) (i : Nat) : M Json := do + -- `i` counts the receiver plus explicit arguments. The receiver is not in + -- `arguments` when the call is `using for`. + let callee ← mField call "expression" + let args ← mArr (← mField call "arguments") + let hasReceiver ← match ← mKind callee with + | "MemberAccess" => + let base ← mField callee "expression" + pure !(← mType base).startsWith "type(library " + | _ => pure false + if hasReceiver then + if i == 0 then mField callee "expression" else pure args[i - 1]! + else + pure args[i]! + +private partial def convert (paramTy argTy : String) (v : Val) (at_ : Json) : M Val := do + match bitsOf paramTy, bitsOf argTy with + | some pb, some sb => + if sb ≤ pb then pure v + else failAt at_ s!"implicit narrowing from {argTy} to {paramTy}" + | _, _ => + if argTy.startsWith "int_const" || argTy == paramTy then pure v + else failAt at_ s!"unsupported implicit conversion from {argTy} to {paramTy}" + +private partial def lowerHelper (stmts : Array Json) (retName : String) : M Val := do + let mut pre : Array String := #[] + let mut result : Option Val := none + for s in stmts do + if result.isSome then failAt s "statement after helper result" + match ← mKind s with + | "VariableDeclarationStatement" => + pre := pre ++ (← lowerLocal s) + | "Return" => + result := some (← lowerExpr (← mField s "expression")) + | "InlineAssembly" => + result := some (← lowerAssembly s retName) + | kind => failAt s s!"unsupported helper statement {kind}" + match result with + | some v => pure { pre := pre ++ v.pre, expr := v.expr } + | none => throwError "inlined helper did not return" + +private partial def lowerAssembly (j : Json) (retName : String) : M Val := do + let ast ← mField j "AST" + unless (← mKind ast) == "YulBlock" do failAt j "assembly is not a Yul block" + let stmts ← mArr (← mField ast "statements") + unless stmts.size == 1 do failAt j "only a single Yul assignment is supported" + let asg := stmts[0]! + unless (← mKind asg) == "YulAssignment" do failAt asg "only a Yul assignment is supported" + let vars ← mArr (← mField asg "variableNames") + unless vars.size == 1 do failAt asg "Yul assignment must have one target" + let vname ← mStr (← mField vars[0]! "name") + unless retName == "" || vname == retName do + failAt asg s!"Yul assigns {vname}, not the return name {retName}" + pure { pre := #[], expr := ← lowerYul (← mField asg "value") } + +private partial def lowerLocal (s : Json) : M (Array String) := do + let decls ← mArr (← mField s "declarations") + unless decls.size == 1 do failAt s "only a single declaration is supported" + if decls[0]!.isNull then failAt s "empty declaration" + let d := decls[0]! + let name ← mStr (← mField d "name") + unless name.all (fun c => c.isAlphanum || c == '_') && name != "" do + failAt d s!"unsupported local name {name}" + let id ← mNat (← mField d "id") + let loc := optStr d "storageLocation" |>.getD "default" + let init ← mField s "initialValue" + if loc == "storage" then + match ← lowerRef init with + | .path pre path => + modify fun e => { e with paths := e.paths.insert id path } + pure pre + | _ => failAt s "storage local is not a resolved read path" + else + let v ← lowerExpr init + modify fun e => { e with values := e.values.insert id (ex s!"localVar {leanStr name}") } + let env ← get + if env.yulNames.contains name then + modify fun e => { e with yulNames := e.yulNames.insert name (ex s!"localVar {leanStr name}") } + pure (v.pre.push (st s!"letVar {leanStr name} {v.expr}")) + +end + +private def structMemberList (st : Json) : M (Array (String × String)) := do + let ms ← mArr (← mField st "members") + let mut out : Array (String × String) := #[] + for m in ms do + out := out.push (← mStr (← mField m "name"), ← mType m) + pure out + +private def bindRoot (fn : Json) : M (Array SrcParam) := do + noteFn fn + let params ← mArr (← mField (← mField fn "parameters") "parameters") + let mut out : Array SrcParam := #[] + for p in params do + let name ← mStr (← mField p "name") + let id ← mNat (← mField p "id") + let ty ← mType p + let loc := optStr p "storageLocation" |>.getD "default" + out := out.push { name, id, typeString := ty, location := loc } + if ty.startsWith "struct " && (loc == "memory" || loc == "calldata") then + let typeName ← mField p "typeName" + let sid ← refInt typeName + if sid < 0 then failAt p "builtin struct" + let some decl := (← get).structs.find? sid.toNat | failAt p s!"unresolved struct {ty}" + let members ← structMemberList decl + let sname ← mStr (← mField decl "name") + modify fun e => + let bound : MemParam := ⟨name, sname, members⟩ + { e with mems := e.mems.insert id bound } + else if ty.startsWith "struct " then + failAt p s!"unsupported location {loc} for {ty}" + else + let some tySyn := paramTypeSyntax ty | failAt p s!"unsupported parameter type {ty}" + modify fun e => + let values := e.values.insert id (ex s!"param {leanStr name}") + let scalarTy := e.scalarTy.insert id tySyn + { e with values, scalarTy } + pure out + +private def lowerRoot (fn : Json) : M (Array String × Array SrcParam) := do + let srcParams ← bindRoot fn + let mods ← mArr (← mField fn "modifiers") + unless mods.isEmpty do failAt fn "modifiers are outside this slice" + let some _ := field? fn "body" | failAt fn "function has no body" + let stmts ← mArr (← mField (← mField fn "body") "statements") + let mut out : Array String := #[] + for s in stmts do + match ← mKind s with + | "VariableDeclarationStatement" => + out := out ++ (← lowerLocal s) + | "Return" => + let expr ← mField s "expression" + if (← mKind expr) == "TupleExpression" then + if ← mBool (← mField expr "isInlineArray") then + failAt expr "inline arrays are outside this slice" + let cs ← mArr (← mField expr "components") + let mut pres : Array String := #[] + let mut exprs : Array String := #[] + for c in cs do + if c.isNull then failAt expr "empty return component" + let v ← atom (← lowerExpr c) + pres := pres ++ v.pre + exprs := exprs.push v.expr + out := out ++ pres |>.push (st s!"returnValues {leanList exprs}") + else + let v ← atom (← lowerExpr expr) + out := out ++ v.pre |>.push (st s!"returnValues {leanList #[v.expr]}") + | kind => failAt s s!"unsupported statement {kind}" + pure (out, srcParams) + +private def keySyntax (ty : String) : MetaM String := + match ty with + | "t_bytes32" => pure "Compiler.CompilationModel.MappingKeyType.bytes32" + | "t_address" => pure "Compiler.CompilationModel.MappingKeyType.address" + | "t_uint256" => pure "Compiler.CompilationModel.MappingKeyType.uint256" + | _ => throwError "unsupported mapping key {ty}" + +private def layoutType (types : Json) (id : String) : MetaM Json := + field types id + +private def buildField (types item : Json) : M FieldInfo := do + let name ← mStr (← mField item "label") + let slot ← mNat (← mField item "slot") + let typeId ← mStr (← mField item "type") + let top ← liftM (layoutType types typeId) + unless (← mStr (← mField top "encoding")) == "mapping" do + throwError "storage field {name} is not a mapping" + let key1 ← liftM (keySyntax (← mStr (← mField top "key"))) + let valueId ← mStr (← mField top "value") + let value ← liftM (layoutType types valueId) + let encoding ← mStr (← mField value "encoding") + let (ctor, key2, structTy) ← + if encoding == "mapping" then + let key2 ← liftM (keySyntax (← mStr (← mField value "key"))) + let innerId ← mStr (← mField value "value") + pure ("mappingStruct2", some key2, ← liftM (layoutType types innerId)) + else if encoding == "inplace" then + pure ("mappingStruct", none, value) + else + throwError "unsupported mapping value encoding {encoding} for {name}" + let members ← mArr (← mField structTy "members") + let mut terms : Array String := #[] + let mut names : Array String := #[] + let mut skipped : Array String := #[] + for m in members do + let label ← mStr (← mField m "label") + let solcType ← mStr (← mField m "type") + let word ← mNat (← mField m "slot") + let byteOff ← mNat (← mField m "offset") + if solcType.startsWith "t_array" || solcType.startsWith "t_mapping" then + skipped := skipped.push label + modify fun e => + let item : OpaqRec := ⟨name, label, solcType, word, byteOff⟩ + { e with opaqueMembers := e.opaqueMembers.push item } + else if solcType.startsWith "t_uint" then + let some bits := (solcType.drop 6).toNat? | throwError "bad uint {solcType}" + let bitOff := byteOff * 8 + unless bitOff + bits ≤ 256 do throwError "packed field {label} does not fit in a word" + let packed := + if bits == 256 && bitOff == 0 then "none" + else + s!"some ({ "{ " }offset := {bitOff}, width := {bits}{ " }" } : Compiler.CompilationModel.PackedBits)" + let sty := + if bits == 16 then "Compiler.CompilationModel.StructMemberType.uint16" + else "Compiler.CompilationModel.StructMemberType.uint256" + terms := terms.push + s!"({ "{ " }name := {leanStr label}, ty := {sty}, wordOffset := {word}, packed := {packed}{ " }" } : Compiler.CompilationModel.StructMember)" + names := names.push label + else + throwError "unsupported layout member {label} : {solcType}" + let tyExpr := + match key2 with + | some k2 => + s!"Compiler.CompilationModel.FieldType.mappingStruct2 {key1} {k2} {leanList terms}" + | none => + s!"Compiler.CompilationModel.FieldType.mappingStruct {key1} {leanList terms}" + let _ := ctor + let term := + s!"({ "{ " }name := {leanStr name}, ty := {tyExpr}, slot := some {slot}{ " }" } : Compiler.CompilationModel.Field)" + pure { name, slot, term, memberNames := names, opaqueNames := skipped } + +private partial def index (file : String) (contract? : Option String) (j : Json) : M Unit := do + match j with + | .obj o => + if (field? j "nodeType").isSome then + if let some idj := field? j "id" then + let id ← mNat idj + modify fun e => { e with nodes := e.nodes.insert id j, nodeFile := e.nodeFile.insert id file } + match ← mKind j with + | "FunctionDefinition" => + modify fun e => + let funs := e.funs.insert id j + let funContract := e.funContract.insert id (contract?.getD "") + { e with funs, funContract } + | "StructDefinition" => + modify fun e => { e with structs := e.structs.insert id j } + | "VariableDeclaration" => + let isState := match field? j "stateVariable" with + | some b => match b.getBool? with | .ok v => v | _ => false + | none => false + if isState then + modify fun e => { e with stateVars := e.stateVars.insert id j } + | _ => pure () + let next ← do + if (field? j "nodeType") == some (Json.str "ContractDefinition") then + let n ← mStr (← mField j "name") + pure (some n) + else + pure contract? + let children := o.foldl (fun acc _ v => v :: acc) [] + for child in children do + index file next child + | .arr xs => + for child in xs do + index file contract? child + | _ => pure () + +private def normalizePath (path : String) : Except String String := Id.run do + let mut out : Array String := #[] + for part in path.splitOn "/" do + if part == "" || part == "." then + pure () + else if part == ".." then + if out.isEmpty then return .error s!"path escapes the project: {path}" + out := out.pop + else + out := out.push part + return .ok (String.intercalate "/" out.toList) + +private def resolveSpec (origin spec : String) (remaps : Array (String × String)) : Except String String := do + if spec.startsWith "." then + let parent := String.intercalate "/" (origin.splitOn "/").dropLast + normalizePath (parent ++ "/" ++ spec) + else + let mut best : Option (String × String) := none + for pair in remaps do + if spec.startsWith pair.1 then + best := match best with + | none => some pair + | some prev => if pair.1.length > prev.1.length then some pair else some prev + match best with + | some (pre, tgt) => normalizePath (tgt ++ spec.drop pre.length) + | none => normalizePath spec + +private def importSpecs (text : String) : Array String := Id.run do + let mut out : Array String := #[] + for line in text.splitOn "\n" do + let t := line.trimAscii.toString + if t.startsWith "import" then + let parts := t.splitOn "\"" + if parts.length ≥ 2 then + out := out.push parts[1]! + pure out + +private def readRemappings (root : System.FilePath) : IO (Array (String × String)) := do + let path := root / "remappings.txt" + if !(← path.pathExists) then return #[] + let text ← IO.FS.readFile path + let mut out : Array (String × String) := #[] + for line in text.splitOn "\n" do + let t := line.trimAscii.toString + if t == "" || t.startsWith "#" || t.startsWith "//" then continue + let parts := t.splitOn "=" + if parts.length == 2 then + out := out.push (parts[0]!, parts[1]!) + pure out + +private partial def collectSources + (root : System.FilePath) (logical : String) + (remaps : Array (String × String)) + (acc : RBMap String String compare) : MetaM (RBMap String String compare) := do + if acc.contains logical then return acc + let path := root / logical + unless ← path.pathExists do throwError "missing Solidity source {logical}" + let text ← IO.FS.readFile path + let mut acc := acc.insert logical text + for spec in importSpecs text do + match resolveSpec logical spec remaps with + | .error e => throwError "{logical}: {e}" + | .ok next => acc ← collectSources root next remaps acc + pure acc + +private def verifyCompiler (compiler : System.FilePath) : MetaM Unit := do + let output ← + if System.Platform.isOSX then + IO.Process.output { cmd := "/usr/bin/shasum", args := #["-a", "256", compiler.toString] } + else + IO.Process.output { cmd := "/usr/bin/sha256sum", args := #[compiler.toString] } + unless output.exitCode == 0 && officialSolcSha256s.contains (output.stdout.take 64).toString do + throwError "compiler checksum mismatch" + +private def moduleText (pkgRoot : System.FilePath) (rel : String) : MetaM String := do + let path := pkgRoot / rel + unless ← path.pathExists do throwError "importer source missing: {path}" + IO.FS.readFile path + +private def renderFn (f : FnRec) : String := + let tys := leanList (f.paramTypes.map leanStr) + s!"({ "{ " }contract := {leanStr f.contract}, name := {leanStr f.name}, declId := {f.declId}, paramTypes := {tys}{ " }" } : Compiler.CompilationModel.SoliditySlice.SliceFunction)" + +private def selectFunction (contract functionName : String) (paramTys : Array String) : M Json := do + let env ← get + let mut hits : Array Json := #[] + let mut described : Array String := #[] + for (id, fn) in env.funs do + let name ← mStr (← mField fn "name") + let owner := env.funContract.find? id |>.getD "" + let params ← mArr (← mField (← mField fn "parameters") "parameters") + let mut tys : Array String := #[] + for p in params do + tys := tys.push (← mType p) + if owner == contract && name == functionName then + described := described.push (String.intercalate ", " tys.toList) + if tys == paramTys then + hits := hits.push fn + if hits.size == 0 then + throwError "no function {contract}.{functionName} matching [{String.intercalate ", " paramTys.toList}]; candidates: {described}" + if hits.size > 1 then + throwError "ambiguous function {contract}.{functionName}" + let fn := hits[0]! + let implemented ← match field? fn "implemented" with + | some b => mBool b + | none => pure true + unless implemented do throwError "function is not implemented" + pure fn + +private def importSlice + (pkgRoot projectRoot : System.FilePath) (entry contract functionName : String) + (paramTys : Array String) (viaIR optimizer : Bool) (evm bytecodeHash : String) + (runs : Nat) : MetaM (String × String × String) := do + let compiler := pkgRoot / ".lake/solidity-import/solc-0.8.34" + verifyCompiler compiler + let versionOut ← IO.Process.output { cmd := compiler.toString, args := #["--version"] } + unless versionOut.exitCode == 0 && + acceptedSolcBanners.contains versionOut.stdout.trimAscii.toString do + throwError "compiler version mismatch" + verifyCompiler compiler + let remaps ← readRemappings projectRoot + let sources ← collectSources projectRoot entry remaps RBMap.empty + let mut fileBytes : RBMap String ByteArray compare := RBMap.empty + let mut sourceObj : Array (String × Json) := #[] + for (logical, text) in sources do + fileBytes := fileBytes.insert logical (text.toUTF8) + sourceObj := sourceObj.push (logical, Json.mkObj [("content", Json.str text)]) + let settings := Json.mkObj [ + ("evmVersion", Json.str evm), + ("metadata", Json.mkObj [("bytecodeHash", Json.str bytecodeHash)]), + ("optimizer", Json.mkObj [("enabled", Json.bool optimizer), ("runs", runs)]), + ("outputSelection", Json.mkObj [("*", Json.mkObj [ + ("", Json.arr #[Json.str "ast"]), + ("*", Json.arr #[Json.str "storageLayout"])])]), + ("remappings", Json.arr (remaps.map fun p => Json.str s!"{p.1}={p.2}")), + ("viaIR", Json.bool viaIR)] + let input := Json.mkObj [ + ("language", Json.str "Solidity"), + ("settings", settings), + ("sources", Json.mkObj sourceObj.toList)] + let output ← IO.Process.output + { cmd := compiler.toString, args := #["--standard-json", "--no-import-callback"] } + (some input.compress) + unless output.exitCode == 0 do throwError "solc failed: {output.stderr}" + verifyCompiler compiler + let parsed ← match Json.parse output.stdout with + | .ok v => pure v + | .error e => throwError "solc output is not JSON: {e}" + if let some errors := field? parsed "errors" then + for err in ← arr errors do + let severity := optStr err "severity" |>.getD "" + if severity == "error" then + throwError "solc: {optStr err "formattedMessage" |>.getD "error"}" + let mut env := Env.init + env := { env with files := fileBytes } + let sourcesOut ← field parsed "sources" + for (logical, _) in sources do + let unit ← field sourcesOut logical + let ast ← field unit "ast" + ((), env) ← (index logical none ast).run env + let contracts ← field parsed "contracts" + let entryContracts ← field contracts entry + let chosen ← field entryContracts contract + let layout ← field chosen "storageLayout" + let types ← field layout "types" + let items ← arr (← field layout "storage") + for item in items do + let label ← str (← field item "label") + let typeId ← str (← field item "type") + let top ← layoutType types typeId + let encoding ← str (← field top "encoding") + if encoding == "mapping" then + let value ← layoutType types (← str (← field top "value")) + let valueEnc ← str (← field value "encoding") + let leaf ← + if valueEnc == "mapping" then + layoutType types (← str (← field value "value")) + else + pure value + if (field? leaf "members").isSome then + let (info, env') ← (buildField types item).run env + env := env' + env := { env with fieldsByName := env.fieldsByName.insert label info } + let fn ← (selectFunction contract functionName paramTys).run' env + env := { env with currentFile := entry } + let ((body, srcParams), env2) ← (lowerRoot fn).run env + env := env2 + let mut modelParams : Array String := #[] + for p in srcParams do + if env.mems.contains p.id then + let some mem := env.mems.find? p.id | throwError "missing memory parameter {p.name}" + let mut seen := false + for (member, ty) in mem.members do + if let some proj := env.projections.find? (fun q => q.parameter == p.name && q.member == member) then + let some tySyn := paramTypeSyntax ty + | throwError "unsupported projected type {ty}" + modelParams := modelParams.push + s!"({ "{ " }name := {leanStr proj.modelParam}, ty := {tySyn}{ " }" } : Compiler.CompilationModel.Param)" + seen := true + unless seen do throwError "struct parameter {p.name} was not read" + else + let some tySyn := env.scalarTy.find? p.id | throwError "missing parameter type" + modelParams := modelParams.push + s!"({ "{ " }name := {leanStr p.name}, ty := {tySyn}{ " }" } : Compiler.CompilationModel.Param)" + let retSyntax ← (do + let rets ← mArr (← mField (← mField fn "returnParameters") "parameters") + let mut out : Array String := #[] + for r in rets do + let ty ← mType r + let some tySyn := paramTypeSyntax ty | throwError "unsupported return type {ty}" + out := out.push tySyn + pure out) |>.run' env + let mutability ← str (← field fn "stateMutability") + let isView := mutability == "view" || mutability == "pure" + let mut fieldTerms : Array (Nat × String) := #[] + for name in env.referenced do + let some info := env.fieldsByName.find? name | throwError "missing field {name}" + fieldTerms := fieldTerms.push (info.slot, info.term) + let sortedFields := (fieldTerms.qsort (fun a b => a.1 < b.1)).map (·.2) + let fnTerm := + s!"({ "{ " }name := {leanStr functionName}, params := {leanList modelParams}, returnType := none, returns := {leanList retSyntax}, isView := {isView}, body := {leanList body}{ " }" } : Compiler.CompilationModel.FunctionSpec)" + let modelName := contract ++ "_" ++ functionName ++ "_slice" + let modelTerm := + s!"({ "{ " }name := {leanStr modelName}, constructor := none, fields := {leanList sortedFields}, functions := [{fnTerm}]{ " }" } : Compiler.CompilationModel.CompilationModel)" + let mut excluded : Array FnRec := #[] + for (id, decl) in env.funs do + unless env.included.any (·.declId == id) do + let name ← str (← field decl "name") + let params ← arr (← field (← field decl "parameters") "parameters") + let mut tys : Array String := #[] + for p in params do + tys := tys.push (← typeString p) + excluded := excluded.push { + contract := env.funContract.find? id |>.getD "", + name, declId := id, paramTypes := tys } + let sortedExcluded := excluded.qsort (fun a b => a.declId < b.declId) + let mut projTerms : Array String := #[] + for proj in env.projections do + let ignored := + match env.mems.toList.find? (fun pair => pair.2.param == proj.parameter) with + | some (_, mem) => mem.members.filterMap fun pair => + if pair.1 == proj.member then none else some (leanStr pair.1) + | none => #[] + projTerms := projTerms.push + s!"({ "{ " }parameter := {leanStr proj.parameter}, member := {leanStr proj.member}, structName := {leanStr proj.structName}, headWord := {proj.headWord}, modelParam := {leanStr proj.modelParam}, ignoredMembers := {leanList ignored}{ " }" } : Compiler.CompilationModel.SoliditySlice.SliceProjection)" + let mut opaqueTerms : Array String := #[] + for o in env.opaqueMembers do + if env.referenced.contains o.field then + opaqueTerms := opaqueTerms.push + s!"({ "{ " }field := {leanStr o.field}, name := {leanStr o.name}, solcType := {leanStr o.solcType}, wordOffset := {o.wordOffset}, byteOffset := {o.byteOffset}{ " }" } : Compiler.CompilationModel.SoliditySlice.OpaqueMember)" + let importer ← moduleText pkgRoot "Compiler/SoliditySlice/Import.lean" + let coverage ← moduleText pkgRoot "Compiler/SoliditySlice/Coverage.lean" + let reportSrc ← moduleText pkgRoot "Compiler/SoliditySlice/Report.lean" + let mut digestInput := s!"{importerVersion}\n{solcLongVersion}\n{contract}\n{functionName}\n" + for ty in paramTys do + digestInput := digestInput ++ ty ++ "\n" + digestInput := digestInput ++ settings.compress ++ "\n" + for (logical, text) in sources.toList.mergeSort (fun a b => a.1 < b.1) do + digestInput := digestInput ++ logical ++ "\n" ++ text ++ "\n" + digestInput := digestInput ++ importer ++ "\n" ++ coverage ++ "\n" ++ reportSrc + let digest := sha256Hex digestInput.toUTF8 + let solcSha := + if System.Platform.isOSX then officialSolcSha256s[1]! else officialSolcSha256s[0]! + let reportTerm := + s!"({ "{ " }importerVersion := {leanStr importerVersion}, solcLongVersion := {leanStr solcLongVersion}, solcSha256 := {leanStr solcSha}, settingsJson := {leanStr settings.compress}, sourceDigest := {leanStr digest}, contract := {leanStr contract}, rootFunction := {leanStr functionName}, includedFunctions := {leanList (env.included.map renderFn)}, excludedFunctions := {leanList (sortedExcluded.map renderFn)}, projections := {leanList projTerms}, storageFields := {leanList (env.referenced.map leanStr)}, opaqueMembers := {leanList opaqueTerms}, observesPanicPayload := false{ " }" } : Compiler.CompilationModel.SoliditySlice.SliceReport)" + pure (modelTerm, reportTerm, digest) + +private def elabDef (name : Name) (type body : String) : CommandElabM Unit := do + let cmd := s!"def {name} : {type} :=\n{body}" + match Parser.runParserCategory (← getEnv) `command cmd "" with + | .error e => throwError e + | .ok stx => elabCommand stx + +syntax (name := sliceImportCmd) + "solidity_slice_import " ident + " slice_root " str " slice_entry " str " slice_contract " str " slice_function " str + " slice_param_tys " "[" str,* "]" + " slice_solc " str " slice_via_ir " ident " slice_evm " str + " slice_optimizer " ident " slice_runs " num " slice_bytecode_hash " str : command + +@[command_elab sliceImportCmd] +def elabSliceImport : CommandElab := fun stx => do + let saved ← getEnv + try + if debug.skipKernelTC.get (← getOptions) then + throwError "kernel checking must be enabled" + let aliasName := stx[1].getId + let rootArg := stx[3].isStrLit?.get! + let entryPath := stx[5].isStrLit?.get! + let contractName := stx[7].isStrLit?.get! + let functionName := stx[9].isStrLit?.get! + let paramTys := stx[12].getSepArgs.map fun s => s.isStrLit?.get! + let solcArg := stx[15].isStrLit?.get! + unless solcArg == solcLongVersion do + throwError "this importer is pinned to {solcLongVersion}" + let useIR := stx[17].getId == `true + let evmVersion := stx[19].isStrLit?.get! + let optimize := stx[21].getId == `true + let some runCount := stx[23].isNatLit? | throwError "optimizer runs must be a nat" + let hashMode := stx[25].isStrLit?.get! + unless useIR || stx[17].getId == `false do throwError "viaIR must be true or false" + unless optimize || stx[21].getId == `false do throwError "optimizer must be true or false" + let authored ← IO.FS.realPath (← getFileName) + let mut pkg := authored.parent.getD "." + while !(← (pkg / "lakefile.lean").pathExists) do + let some parent := pkg.parent | throwError "package root not found" + if parent == pkg then throwError "package root not found" + pkg := parent + let project := if ("/" ++ rootArg).startsWith "//" || rootArg.startsWith "/" then + System.FilePath.mk rootArg else pkg / rootArg + let (modelTerm, reportTerm, digest) ← liftTermElabM <| + importSlice pkg project entryPath contractName functionName paramTys useIR optimize evmVersion hashMode runCount + let ns := (← getCurrNamespace) ++ aliasName + elabDef (ns ++ `model) "Compiler.CompilationModel.CompilationModel" modelTerm + elabDef (ns ++ `report) "Compiler.CompilationModel.SoliditySlice.SliceReport" reportTerm + elabDef (ns ++ `sourceDigest) "String" (leanStr digest) + let covered := s!"theorem {ns ++ `sliceCovered} : Compiler.CompilationModel.SoliditySlice.modelSliceCovered {ns ++ `model} = true := by decide" + match Parser.runParserCategory (← getEnv) `command covered "" with + | .error e => throwError e + | .ok stx => elabCommand stx + if (← get).messages.hasErrors then + throwError "slice import failed to check" + catch e => + setEnv saved + throw e + +end Compiler.CompilationModel.SoliditySlice diff --git a/Compiler/SoliditySlice/Report.lean b/Compiler/SoliditySlice/Report.lean new file mode 100644 index 0000000000..c1a44969e6 --- /dev/null +++ b/Compiler/SoliditySlice/Report.lean @@ -0,0 +1,59 @@ +/-! +Report of a Solidity function slice lowered to `CompilationModel`. + +A slice is not a whole-contract invariant. `opaqueMembers` are layout records +copied from solc and deliberately not given an executable encoding. +-/ +namespace Compiler.CompilationModel.SoliditySlice + +/-- One function whose body was translated. Library helpers are included even +when the executable model inlines them. -/ +structure SliceFunction where + contract : String + name : String + declId : Nat + paramTypes : List String + deriving Repr, BEq + +/-- A memory-struct parameter replaced by the static members the slice reads. +`headWord` is the member's index in the Solidity memory/ABI head, derived from +the struct declaration order. This is not an ABI decoder. -/ +structure SliceProjection where + parameter : String + member : String + structName : String + headWord : Nat + modelParam : String + ignoredMembers : List String + deriving Repr, BEq + +/-- A struct member present in solc's storage layout that this slice does not encode. -/ +structure OpaqueMember where + field : String + name : String + solcType : String + wordOffset : Nat + byteOffset : Nat + deriving Repr, BEq + +/-- Provenance and closure report. `sourceDigest` covers sources, solc settings, +the compiler release identity, and the importer sources. -/ +structure SliceReport where + importerVersion : String + solcLongVersion : String + solcSha256 : String + settingsJson : String + sourceDigest : String + contract : String + rootFunction : String + includedFunctions : List SliceFunction + excludedFunctions : List SliceFunction + projections : List SliceProjection + storageFields : List String + opaqueMembers : List OpaqueMember + /-- Proof denotation records success versus revert. `Stmt.panic` keeps its + `PanicCode` on the statement, and `execStmt` does not return that payload. -/ + observesPanicPayload : Bool := false + deriving Repr, BEq + +end Compiler.CompilationModel.SoliditySlice diff --git a/Contracts/SoliditySliceSmoke/Lib.sol b/Contracts/SoliditySliceSmoke/Lib.sol new file mode 100644 index 0000000000..e72cb2c657 --- /dev/null +++ b/Contracts/SoliditySliceSmoke/Lib.sol @@ -0,0 +1,26 @@ +// SPDX-License-Identifier: MIT +pragma solidity 0.8.34; + +library L { + function min(uint256 x, uint256 y) internal pure returns (uint256 z) { + assembly { + z := xor(x, mul(xor(x, y), lt(y, x))) + } + } + + function mulDivDown(uint256 x, uint256 y, uint256 d) internal pure returns (uint256) { + return (x * y) / d; + } + + function mulDivUp(uint256 x, uint256 y, uint256 d) internal pure returns (uint256) { + return (x * y + (d - 1)) / d; + } + + function unused() internal pure returns (uint256) { + uint256 s = 0; + for (uint256 i = 0; i < 3; i++) { + s += i; + } + return s; + } +} diff --git a/Contracts/SoliditySliceSmoke/Slice.sol b/Contracts/SoliditySliceSmoke/Slice.sol new file mode 100644 index 0000000000..7a323657a2 --- /dev/null +++ b/Contracts/SoliditySliceSmoke/Slice.sol @@ -0,0 +1,53 @@ +// SPDX-License-Identifier: MIT +pragma solidity 0.8.34; + +import "./Lib.sol"; + +struct Pos { + uint128 credit; + uint128 pendingFee; + uint128 lastLossFactor; + uint128 lastAccrual; + uint128 debt; + uint128 collateralBitmap; +} + +struct MktState { + uint128 totalUnits; + uint128 lossFactor; +} + +struct Mkt { + uint256 ignored; + uint128[] collateralParams; + uint256 maturity; +} + +contract C { + using L for uint256; + using L for uint128; + + mapping(bytes32 => mapping(address => Pos)) public position; + mapping(bytes32 => MktState) public marketState; + + function f(Mkt memory m, bytes32 id, address user) + external + view + returns (uint128, uint128, uint128) + { + Pos storage p = position[id][user]; + uint128 credit = p.credit; + uint128 lastLoss = p.lastLossFactor; + uint256 post = lastLoss < type(uint128).max + ? credit.mulDivDown(type(uint128).max - marketState[id].lossFactor, type(uint128).max - lastLoss) + : 0; + uint128 pending = p.pendingFee; + uint256 postFee = credit > 0 ? pending - pending.mulDivUp(credit - post, credit) : 0; + uint256 end = L.min(block.timestamp, m.maturity); + uint128 lastAccrual = p.lastAccrual; + uint128 fee = lastAccrual < m.maturity + ? uint128(postFee.mulDivDown(end - lastAccrual, m.maturity - lastAccrual)) + : 0; + return (uint128(post) - fee, uint128(postFee) - fee, fee); + } +} diff --git a/Contracts/SoliditySliceSmoke/Smoke.lean b/Contracts/SoliditySliceSmoke/Smoke.lean new file mode 100644 index 0000000000..776edbd49d --- /dev/null +++ b/Contracts/SoliditySliceSmoke/Smoke.lean @@ -0,0 +1,98 @@ +import Compiler.SoliditySlice.Import +import Compiler.SoliditySlice.Coverage + +open Compiler.CompilationModel +open Compiler.CompilationModel.SoliditySlice +open Compiler.CompilationModel.Denote + +solidity_slice_import slice + slice_root "Contracts/SoliditySliceSmoke" slice_entry "Slice.sol" + slice_contract "C" slice_function "f" + slice_param_tys ["struct Mkt", "bytes32", "address"] + slice_solc "0.8.34+commit.80d5c536" slice_via_ir true slice_evm "osaka" + slice_optimizer true slice_runs 466 slice_bytecode_hash "none" + +/-- Oracle used only to place the two struct words of this witness. +The numeric claim is about the values `structMember` reads back. -/ +def sliceOracle : DenoteOracle where + mappingSlot base key := base + key + 1 + keccakMemorySlice _ _ _ := 0 + +def packHalves (lo hi : Nat) : Nat := lo + hi * 2 ^ 128 + +/-- `id = 1`, `user = 2`. Position words land at slots 5 and 6; the market +state word lands at slot 3. -/ +def witnessWorld (credit pending lossFactor lastLoss lastAccrual : Nat) : Verity.ContractState := + Verity.defaultState.withStorageWords fun key => + let word := + match key with + | .slot 5 => packHalves credit pending + | .slot 6 => packHalves lastLoss lastAccrual + | .slot 3 => packHalves 0 lossFactor + | _ => 0 + Verity.Core.Uint256.ofNat word + +def witnessBindings (maturity : Nat) : Env := + [("m_maturity", maturity), ("id", 1), ("user", 2)] + +private def smokeBody : List Stmt := + match slice.model.functions with + | fn :: _ => fn.body + | [] => [] + +def peel (stmts : List Stmt) : Stmt × List Stmt := + match stmts with + | s :: r => (s, r) + | [] => (.panic .arithmeticOverflow, []) + +theorem smokeBody_peel : smokeBody = (peel smokeBody).1 :: (peel smokeBody).2 := by + unfold smokeBody peel + rfl + +def witnessState (credit pending lossFactor lastLoss lastAccrual timestamp maturity : Nat) : DenoteState := + { world := + { witnessWorld credit pending lossFactor lastLoss lastAccrual with + blockTimestamp := Verity.Core.Uint256.ofNat timestamp } + bindings := witnessBindings maturity } + +theorem string_beq_kernel : ("m_maturity" == "id") = false := by + decide + +set_option maxHeartbeats 2000000 in +theorem smoke_step_credit : + execStmt sliceOracle slice.model.fields (witnessState 100 10 0 0 0 50 100) (peel smokeBody).1 = + .continue + { witnessState 100 10 0 0 0 50 100 with + bindings := bindValue (witnessBindings 100) "credit" 100 } := by + dsimp only [peel, smokeBody, slice.model] + have hmi : ("m_maturity" == "id") = false := by decide + have hmu : ("m_maturity" == "user") = false := by decide + have hiu : ("id" == "user") = false := by decide + have hft : (false = true) = False := by decide + have h5 : 5 % Verity.Core.Uint256.modulus = 5 := Nat.mod_eq_of_lt (by decide) + have h0m : 0 % Verity.Core.Uint256.modulus = 0 := Nat.mod_eq_of_lt (by decide) + simp only [hmi, hmu, hiu, hft, h5, h0m, Nat.mod_mod, Nat.shiftRight_zero, + execStmt, evalExpr, findFieldWithResolvedSlot_eq_CopyFrom, + findFieldWithResolvedSlotCopyFrom, findStructMembers, findStructMember, readFieldWord, + packedMaskNat, lookupValue, sliceOracle, witnessState, witnessWorld, witnessBindings, + packHalves, bindValue, wordNormalize, Verity.ContractState.withStorageWords, + Verity.ContractState.readSlot, Verity.ContractState.storage, Verity.Core.Uint256.shr, + Verity.Core.Uint256.and, Verity.Core.Uint256.ofNat, bind, Option.bind, Option.map, + Option.getD, List.find?, beq_self_eq_true, ↓reduceIte, + ↓Nat.reduceAdd, ↓Nat.reduceMul, ↓Nat.reducePow, ↓Nat.reduceSub] + have hcredit : + (3402823669209384634633746074317682114660 % Verity.Core.Uint256.modulus &&& + 340282366920938463463374607431768211455 % Verity.Core.Uint256.modulus) % + Verity.Core.Uint256.modulus = 100 := by decide + simp [hcredit] + +-- Interpreter checks. Not kernel proofs. `smoke_step_credit` proves the first read. +#eval show IO Unit from do + let success := denoteScalarBody sliceOracle slice.model.fields + (witnessWorld 100 10 0 0 0) 50 (witnessBindings 100) smokeBody + unless success == some [95, 5, 5] do + throw (IO.userError s!"witness A returned {success}") + let reverted := denoteScalarBody sliceOracle slice.model.fields + (witnessWorld 1 2 0 0 0) 1 (witnessBindings 1) smokeBody + unless reverted == none do + throw (IO.userError s!"witness B returned {reverted}") diff --git a/TRUST_ASSUMPTIONS.md b/TRUST_ASSUMPTIONS.md index 4b21049252..4d85b90a22 100644 --- a/TRUST_ASSUMPTIONS.md +++ b/TRUST_ASSUMPTIONS.md @@ -2,6 +2,34 @@ This document states what Verity proves and what it still trusts. +## Solidity function-slice import + +`Compiler/SoliditySlice/Import.lean` selects one function from a solc project, +closes over the definitions solc's declaration ids actually reach, and elaborates +a `CompilationModel`. Execution of an accepted slice is +`Compiler.CompilationModel.Denote.execStmt`, restricted by +`stmtListCovered` in `Compiler/SoliditySlice/Coverage.lean`. That predicate is +not `SupportedFunction`: the slice uses `panic`, `returnValues`, packed +`structMember` / `structMember2`, and checked arithmetic, which the IR raccord +does not cover. `DenoteAgreement.execStmt_eq` still holds, including +`Stmt.returnValues`, which records source-order words on `DenoteState` and +finishes as `.stop`. The proof denotation observes success versus revert. It +does not observe the `PanicCode` payload. + +The frontend is in the trust base. A covered model is not a proof that the +model matches the Solidity source or solc's bytecode. `storageLayout` slots and +packed offsets are copied from pinned solc 0.8.34 +(`0.8.34+commit.80d5c536`; linux-amd64 and macosx-amd64 SHA-256 pins in +`Import.lean`). `mappingSlot` in the denotation oracle is not Keccak. Numeric +claims are about the values `structMember` reads back. The Vault importer's +solc 0.8.33 pin is unchanged. + +Checked `uint256` and narrower unsigned arithmetic is lowered to `Stmt.ite` plus +`Stmt.panic`, not to a second interpreter. Explicit `uint128(x)` is truncation +(`bitAnd` with `2^128-1`). Ternaries are `Stmt.ite`, so the untaken branch is +not evaluated. Library helpers in an accepted acyclic slice are inlined. +`UtilsLib`-style `min` is the Yul term `xor`/`mul`/`lt`, not a renamed `Expr.min`. + ## Proof-only Solidity Vault import This POC is separate from the verified compilation pipeline below. It trusts diff --git a/lakefile.lean b/lakefile.lean index cb41bf7d54..9b25773a76 100644 --- a/lakefile.lean +++ b/lakefile.lean @@ -120,6 +120,9 @@ lean_lib «Contracts» where .andSubmodules `Contracts.ReentrancyRelyGuarantee ] +lean_lib «SoliditySliceSmoke» where + globs := #[.one `Contracts.SoliditySliceSmoke.Smoke] + lean_lib «Compiler» where globs := #[.andSubmodules `Compiler] diff --git a/scripts/setup_solc_slice.py b/scripts/setup_solc_slice.py new file mode 100644 index 0000000000..f64499a2c1 --- /dev/null +++ b/scripts/setup_solc_slice.py @@ -0,0 +1,92 @@ +#!/usr/bin/env python3 +"""Install pinned solc 0.8.34 for the function-slice importer. + +The Vault importer keeps solc 0.8.33 at `.lake/solidity-import/solc`. +This script writes `.lake/solidity-import/solc-0.8.34` and does not replace it. +Lake elaboration never downloads the compiler. +""" + +from __future__ import annotations + +import hashlib +import platform +import sys +import urllib.request +from pathlib import Path + +ROOT = Path(__file__).resolve().parents[1] +DEST = ROOT / ".lake" / "solidity-import" / "solc-0.8.34" +LONG_VERSION = "0.8.34+commit.80d5c536" +SHA256 = { + "linux-amd64": "d40adc6f9fdbb22a97d32a02fa05688bf2ee7886affc48c9851b0afd4a726b39", + "macosx-amd64": "0a2829292697dda542e4e365bb63fbd6d3ed51537140222a880ab760cffa7746", +} + + +def host_platform() -> str: + if sys.platform == "darwin": + return "macosx-amd64" + if sys.platform.startswith("linux") and platform.machine() in ("x86_64", "amd64"): + return "linux-amd64" + raise RuntimeError(f"no solc 0.8.34 pin for {sys.platform} {platform.machine()}") + + +def file_sha256(path: Path) -> str: + digest = hashlib.sha256() + with path.open("rb") as handle: + for chunk in iter(lambda: handle.read(1024 * 1024), b""): + digest.update(chunk) + return digest.hexdigest() + + +def install(platform_name: str) -> None: + expected = SHA256[platform_name] + list_url = f"https://binaries.soliditylang.org/{platform_name}/list.json" + request = urllib.request.Request(list_url, headers={"User-Agent": "verity-official-solc/0.8.34"}) + import json + with urllib.request.urlopen(request, timeout=60) as response: + payload = json.load(response) + match = None + for entry in payload.get("builds", []): + if entry.get("longVersion") == LONG_VERSION: + match = entry + break + if match is None: + raise RuntimeError(f"list.json has no build for {LONG_VERSION}") + published = str(match["sha256"]) + if published.startswith("0x"): + published = published[2:] + if published != expected: + raise RuntimeError(f"list.json SHA-256 {published} != pin {expected}") + url = f"https://binaries.soliditylang.org/{platform_name}/{match['path']}" + DEST.parent.mkdir(parents=True, exist_ok=True) + tmp = DEST.with_name(DEST.name + ".tmp") + try: + binary = urllib.request.Request(url, headers={"User-Agent": "verity-official-solc/0.8.34"}) + with urllib.request.urlopen(binary, timeout=60) as response, tmp.open("wb") as handle: + handle.write(response.read()) + got = file_sha256(tmp) + if got != expected: + raise RuntimeError(f"downloaded SHA-256 {got} != {expected}") + tmp.replace(DEST) + DEST.chmod(0o755) + finally: + if tmp.exists(): + tmp.unlink() + print(f"installed {LONG_VERSION} ({platform_name}) at {DEST}") + + +def main() -> int: + if DEST.exists() and file_sha256(DEST) == SHA256[host_platform()]: + print(f"already installed at {DEST}") + return 0 + install(host_platform()) + return 0 + + +if __name__ == "__main__": + try: + raise SystemExit(main()) + except Exception as exc: + print(f"error: {exc}", file=sys.stderr) + raise SystemExit(1) From f58aff9cee7c5bb7ffb31e305ee9a27720073e18 Mon Sep 17 00:00:00 2001 From: "github-actions[bot]" Date: Tue, 22 Sep 2026 18:42:17 +0100 Subject: [PATCH 03/22] chore: auto-refresh derived artifacts --- artifacts/verification_status.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/artifacts/verification_status.json b/artifacts/verification_status.json index e084577f11..c6c9656db5 100644 --- a/artifacts/verification_status.json +++ b/artifacts/verification_status.json @@ -1,7 +1,7 @@ { "codebase": { "core_lines": 2257, - "example_contracts": 20 + "example_contracts": 21 }, "proofs": { "axioms": 1, From d489c37adef68f3d020fd7f612b69138e8a08339 Mon Sep 17 00:00:00 2001 From: Thomas Marchand Date: Tue, 22 Sep 2026 18:53:41 +0100 Subject: [PATCH 04/22] Hash the slice importer from the Verity checkout in downstream packages. A package that depends on Verity elaborates solidity_slice_import against .lake/packages/verity, so the provenance digest has to read those sources there. The pinned solc binary stays in the elaborating package. Co-authored-by: Cursor --- AUDIT.md | 5 ++++- Compiler/SoliditySlice/Import.lean | 18 +++++++++++++++--- TRUST_ASSUMPTIONS.md | 6 ++++++ 3 files changed, 25 insertions(+), 4 deletions(-) diff --git a/AUDIT.md b/AUDIT.md index 1a6291a7e7..29bad4056d 100644 --- a/AUDIT.md +++ b/AUDIT.md @@ -19,7 +19,10 @@ of the first packed `credit` read. It is not a proof of the whole body, and it is not a proof that the model matches solc bytecode. The slice compiler pin is solc `0.8.34+commit.80d5c536`, installed separately -from the Vault 0.8.33 pin by `python3 scripts/setup_solc_slice.py`. +from the Vault 0.8.33 pin by `python3 scripts/setup_solc_slice.py`. The source +digest reads the importer modules from the Verity tree, or from +`.lake/packages/verity` when a downstream package elaborates the command. The +solc binary remains the elaborating package's `.lake/solidity-import/solc-0.8.34`. Evidence command: `lake build SoliditySliceSmoke` after the 0.8.34 compiler is installed at `.lake/solidity-import/solc-0.8.34`. diff --git a/Compiler/SoliditySlice/Import.lean b/Compiler/SoliditySlice/Import.lean index eda2a2cdfd..4225c40630 100644 --- a/Compiler/SoliditySlice/Import.lean +++ b/Compiler/SoliditySlice/Import.lean @@ -966,6 +966,17 @@ private def verifyCompiler (compiler : System.FilePath) : MetaM Unit := do unless output.exitCode == 0 && officialSolcSha256s.contains (output.stdout.take 64).toString do throwError "compiler checksum mismatch" +/-- Verity itself stores the importer next to its lakefile. A downstream package +stores that checkout at `.lake/packages/verity`. The digest must hash those +sources in either layout. -/ +private def importerSourceRoot (pkgRoot : System.FilePath) : MetaM System.FilePath := do + if ← (pkgRoot / "Compiler/SoliditySlice/Import.lean").pathExists then + return pkgRoot + let dep := pkgRoot / ".lake/packages/verity" + if ← (dep / "Compiler/SoliditySlice/Import.lean").pathExists then + return dep + throwError "importer source missing: {pkgRoot / "Compiler/SoliditySlice/Import.lean"}" + private def moduleText (pkgRoot : System.FilePath) (rel : String) : MetaM String := do let path := pkgRoot / rel unless ← path.pathExists do throwError "importer source missing: {path}" @@ -1142,9 +1153,10 @@ private def importSlice if env.referenced.contains o.field then opaqueTerms := opaqueTerms.push s!"({ "{ " }field := {leanStr o.field}, name := {leanStr o.name}, solcType := {leanStr o.solcType}, wordOffset := {o.wordOffset}, byteOffset := {o.byteOffset}{ " }" } : Compiler.CompilationModel.SoliditySlice.OpaqueMember)" - let importer ← moduleText pkgRoot "Compiler/SoliditySlice/Import.lean" - let coverage ← moduleText pkgRoot "Compiler/SoliditySlice/Coverage.lean" - let reportSrc ← moduleText pkgRoot "Compiler/SoliditySlice/Report.lean" + let sourceRoot ← importerSourceRoot pkgRoot + let importer ← moduleText sourceRoot "Compiler/SoliditySlice/Import.lean" + let coverage ← moduleText sourceRoot "Compiler/SoliditySlice/Coverage.lean" + let reportSrc ← moduleText sourceRoot "Compiler/SoliditySlice/Report.lean" let mut digestInput := s!"{importerVersion}\n{solcLongVersion}\n{contract}\n{functionName}\n" for ty in paramTys do digestInput := digestInput ++ ty ++ "\n" diff --git a/TRUST_ASSUMPTIONS.md b/TRUST_ASSUMPTIONS.md index 4d85b90a22..35782eb841 100644 --- a/TRUST_ASSUMPTIONS.md +++ b/TRUST_ASSUMPTIONS.md @@ -16,6 +16,12 @@ does not cover. `DenoteAgreement.execStmt_eq` still holds, including finishes as `.stop`. The proof denotation observes success versus revert. It does not observe the `PanicCode` payload. +The source digest hashes `Import.lean`, `Coverage.lean`, and `Report.lean` from +the package that contains them: the Verity tree itself, or +`.lake/packages/verity` when a downstream package elaborates the import. The +pinned solc binary stays in the elaborating package's +`.lake/solidity-import/solc-0.8.34`. + The frontend is in the trust base. A covered model is not a proof that the model matches the Solidity source or solc's bytecode. `storageLayout` slots and packed offsets are copied from pinned solc 0.8.34 From c8764d6b9ee09480fbf0227b2f38c5e006686fd3 Mon Sep 17 00:00:00 2001 From: Thomas Marchand Date: Tue, 22 Sep 2026 19:26:48 +0100 Subject: [PATCH 05/22] Record slice-import mutation checks against the smoke project. An unreached loop still imports, a reached loop is rejected without leaving an olean, and helper, factor, field, return-order, and layout edits are detected by the witness. Co-authored-by: Cursor --- AUDIT.md | 8 +- scripts/solidity_slice_mutations.py | 207 ++++++++++++++++++++++++++++ 2 files changed, 213 insertions(+), 2 deletions(-) create mode 100644 scripts/solidity_slice_mutations.py diff --git a/AUDIT.md b/AUDIT.md index 29bad4056d..ff7b75b175 100644 --- a/AUDIT.md +++ b/AUDIT.md @@ -24,8 +24,12 @@ digest reads the importer modules from the Verity tree, or from `.lake/packages/verity` when a downstream package elaborates the command. The solc binary remains the elaborating package's `.lake/solidity-import/solc-0.8.34`. -Evidence command: `lake build SoliditySliceSmoke` after the 0.8.34 compiler is -installed at `.lake/solidity-import/solc-0.8.34`. +Evidence commands: `lake build SoliditySliceSmoke` after the 0.8.34 compiler is +installed at `.lake/solidity-import/solc-0.8.34`, and +`python3 scripts/solidity_slice_mutations.py`. The mutation script checks an +unreached `for`, rejection of that `for` once it is reachable, and detection of +a reached-helper change, a fee-factor change, a field change, a return-order +change, and a layout swap. A rejected import leaves no `.olean`. ## Proof-only Solidity Vault POC diff --git a/scripts/solidity_slice_mutations.py b/scripts/solidity_slice_mutations.py new file mode 100644 index 0000000000..3dbe04d948 --- /dev/null +++ b/scripts/solidity_slice_mutations.py @@ -0,0 +1,207 @@ +#!/usr/bin/env python3 +"""Mutation checks for the Solidity slice importer. + +Each case copies the smoke project, changes one thing, and elaborates +`solidity_slice_import`. A reached helper, a return-order change, and a layout +change must make the witness check fail. An unreached `for` stays importable. +Reaching that `for` must be rejected with a source position, and the failed +process must not leave an olean. +""" + +import shutil +import subprocess +import sys +from pathlib import Path + +ROOT = Path(__file__).resolve().parents[1] +SOLC = ROOT / ".lake/solidity-import/solc-0.8.34" +WORK = ROOT / ".lake/slice-mutations" +SMOKE = ROOT / "Contracts/SoliditySliceSmoke" + +HEADER = """ +import Compiler.SoliditySlice.Import +import Compiler.SoliditySlice.Coverage + +open Compiler.CompilationModel +open Compiler.CompilationModel.SoliditySlice +open Compiler.CompilationModel.Denote + +solidity_slice_import imported + slice_root "{root}" slice_entry "Slice.sol" + slice_contract "C" slice_function "f" + slice_param_tys ["struct Mkt", "bytes32", "address"] + slice_solc "0.8.34+commit.80d5c536" slice_via_ir true slice_evm "osaka" + slice_optimizer true slice_runs 466 slice_bytecode_hash "none" +""" + +WITNESS = r""" +def sliceOracle : DenoteOracle where + mappingSlot base key := base + key + 1 + keccakMemorySlice _ _ _ := 0 + +def packHalves (lo hi : Nat) : Nat := lo + hi * 2 ^ 128 + +def witnessWorld (credit pending lossFactor lastLoss lastAccrual : Nat) : Verity.ContractState := + Verity.defaultState.withStorageWords fun key => + let word := + match key with + | .slot 5 => packHalves credit pending + | .slot 6 => packHalves lastLoss lastAccrual + | .slot 3 => packHalves 0 lossFactor + | _ => 0 + Verity.Core.Uint256.ofNat word + +def witnessBindings (maturity : Nat) : Env := + [("m_maturity", maturity), ("id", 1), ("user", 2)] + +def body : List Stmt := + match imported.model.functions with + | fn :: _ => fn.body + | [] => [] + +#eval IO.println imported.report.sourceDigest + +#eval show IO Unit from do + let success := denoteScalarBody sliceOracle imported.model.fields + (witnessWorld 100 10 0 0 0) 50 (witnessBindings 100) body + unless success == some [95, 5, 5] do + throw (IO.userError s!"witness changed: {success}") + IO.println imported.report.sourceDigest +""" + + +def lean(path: Path) -> subprocess.CompletedProcess[str]: + return subprocess.run( + ["lake", "env", "lean", str(path)], + cwd=ROOT, + text=True, + capture_output=True, + ) + + +def write_project(name: str, mutate) -> Path: + dest = WORK / name + if dest.exists(): + shutil.rmtree(dest) + dest.mkdir(parents=True) + shutil.copy(SMOKE / "Slice.sol", dest / "Slice.sol") + shutil.copy(SMOKE / "Lib.sol", dest / "Lib.sol") + mutate(dest) + return dest + + +def expect_success(name: str, project: Path, witness: bool) -> str: + lean_path = project / "Check.lean" + text = HEADER.format(root=project) + if witness: + text += WITNESS + lean_path.write_text(text) + result = lean(lean_path) + if result.returncode != 0: + sys.stderr.write(result.stdout) + sys.stderr.write(result.stderr) + raise SystemExit(f"{name}: expected success") + print(f"pass {name}") + return result.stdout + + +def expect_failure(name: str, project: Path, needle: str) -> str: + lean_path = project / "Check.lean" + olean = project / "Check.olean" + if olean.exists(): + olean.unlink() + lean_path.write_text(HEADER.format(root=project) + WITNESS) + result = subprocess.run( + ["lake", "env", "lean", str(lean_path), "-o", str(olean)], + cwd=ROOT, + text=True, + capture_output=True, + ) + blob = result.stdout + result.stderr + if result.returncode == 0 or needle not in blob or olean.exists(): + sys.stderr.write(blob) + raise SystemExit(f"{name}: expected rejection containing {needle!r}") + print(f"pass {name}") + return blob + + +def unchanged(dest: Path) -> None: + return None + + +def call_unused(dest: Path) -> None: + text = (dest / "Slice.sol").read_text() + text = text.replace( + "uint128 credit = p.credit;", + "uint256 ignored = L.unused();\n uint128 credit = p.credit;", + ) + (dest / "Slice.sol").write_text(text) + + +def swap_returns(dest: Path) -> None: + text = (dest / "Slice.sol").read_text() + text = text.replace( + "return (uint128(post) - fee, uint128(postFee) - fee, fee);", + "return (fee, uint128(postFee) - fee, uint128(post) - fee);", + ) + (dest / "Slice.sol").write_text(text) + + +def change_rounding(dest: Path) -> None: + # Witness A divides evenly, so floor versus ceil is invisible there. + # An extra division in the reached helper changes that witness. + text = (dest / "Lib.sol").read_text() + text = text.replace("return (x * y) / d;", "return (x * y) / d / 2;") + (dest / "Lib.sol").write_text(text) + + +def change_factor(dest: Path) -> None: + text = (dest / "Slice.sol").read_text() + text = text.replace( + "postFee.mulDivDown(end - lastAccrual, m.maturity - lastAccrual)", + "postFee.mulDivDown(end - lastAccrual + 50, m.maturity - lastAccrual)", + ) + (dest / "Slice.sol").write_text(text) + + +def change_field(dest: Path) -> None: + text = (dest / "Slice.sol").read_text() + text = text.replace("uint128 credit = p.credit;", "uint128 credit = p.pendingFee;") + (dest / "Slice.sol").write_text(text) + + +def swap_layout(dest: Path) -> None: + text = (dest / "Slice.sol").read_text() + text = text.replace( + "uint128 credit;\n uint128 pendingFee;", + "uint128 pendingFee;\n uint128 credit;", + ) + (dest / "Slice.sol").write_text(text) + + +def digest_of(blob: str) -> str: + lines = [line.strip() for line in blob.splitlines() if len(line.strip()) == 64] + if not lines: + raise SystemExit(f"no digest in output:\n{blob}") + return lines[-1] + + +def main() -> None: + if not SOLC.is_file(): + raise SystemExit(f"missing {SOLC}") + WORK.mkdir(parents=True, exist_ok=True) + base = expect_success("unreached-for", write_project("base", unchanged), witness=True) + rounding = expect_failure("rounding-detected", write_project("rounding", change_rounding), "witness changed") + expect_failure("factor-detected", write_project("factor", change_factor), "witness changed") + expect_failure("field-detected", write_project("field", change_field), "witness changed") + expect_failure("return-order", write_project("returns", swap_returns), "witness changed") + expect_failure("layout", write_project("layout", swap_layout), "witness changed") + expect_failure("reachable-for", write_project("reachable", call_unused), "unsupported") + if digest_of(base) == digest_of(rounding): + raise SystemExit("helper change did not move the digest") + print("pass helper-digest") + print("slice mutations passed") + + +if __name__ == "__main__": + main() From 7d60b32405d91d45b0d0789902cfa13deea1890e Mon Sep 17 00:00:00 2001 From: Thomas Marchand Date: Wed, 23 Sep 2026 16:49:26 +0200 Subject: [PATCH 06/22] Harden deterministic Solidity slice imports for Midnight delivery --- AUDIT.md | 12 +- .../Proofs/IRGeneration/SourceSemantics.lean | 45 +-- Compiler/SoliditySlice/Import.lean | 300 ++++++++++-------- Compiler/SoliditySlice/README.md | 85 +++++ Compiler/SoliditySlice/Report.lean | 17 + Contracts/SoliditySliceSmoke/Smoke.lean | 14 +- TRUST_ASSUMPTIONS.md | 7 + Verity/Core/Model/Denote.lean | 23 +- artifacts/trust_surface_report.json | 2 +- scripts/setup_solc_slice.py | 22 +- scripts/solidity_slice_mutations.py | 100 +++++- 11 files changed, 402 insertions(+), 225 deletions(-) create mode 100644 Compiler/SoliditySlice/README.md diff --git a/AUDIT.md b/AUDIT.md index ff7b75b175..0fec1c6699 100644 --- a/AUDIT.md +++ b/AUDIT.md @@ -13,7 +13,7 @@ on `DenoteState.observedReturnWords` and is mirrored in `SourceSemantics` and `DenoteAgreement`. Accepted slices carry `sliceCovered`, proved by `decide` against `modelSliceCovered`. The smoke contract `Contracts/SoliditySliceSmoke` contains an unreached `for` loop; the import -still succeeds. `#eval` checks the interpreter result of two witnesses on that +still succeeds. The mutation harness checks the interpreter result of two witnesses on that imported body: `(95, 5, 5)` and a revert. `smoke_step_credit` is a kernel proof of the first packed `credit` read. It is not a proof of the whole body, and it is not a proof that the model matches solc bytecode. @@ -29,7 +29,15 @@ installed at `.lake/solidity-import/solc-0.8.34`, and `python3 scripts/solidity_slice_mutations.py`. The mutation script checks an unreached `for`, rejection of that `for` once it is reachable, and detection of a reached-helper change, a fee-factor change, a field change, a return-order -change, and a layout swap. A rejected import leaves no `.olean`. +change, and a layout swap. It also covers hygienic helper locals, generated-name +collisions, namespaces, repeated deterministic imports, unrelated unsupported +layouts, and fail-closed rejection of named arguments and implicit returns. +A rejected import leaves no `.olean`. + +The downstream `morpho-midnight-verity` pilot proves all five CVL assertions on +actual imported execution, with arbitrary state/oracle and mathematical integer +ghosts. That proof adds no source-to-model equivalence claim. See +`Compiler/SoliditySlice/README.md` for the supported subset and freshness checks. ## Proof-only Solidity Vault POC diff --git a/Compiler/Proofs/IRGeneration/SourceSemantics.lean b/Compiler/Proofs/IRGeneration/SourceSemantics.lean index bbff2c0184..252513868e 100644 --- a/Compiler/Proofs/IRGeneration/SourceSemantics.lean +++ b/Compiler/Proofs/IRGeneration/SourceSemantics.lean @@ -1193,22 +1193,6 @@ def evalExpr (fields : List Field) (state : RuntimeState) : Expr → Option Nat | .paramDynamicMemberDataOffset _ _ => none | .paramDynamicMemberElement _ _ _ => none | .paramDynamicStaticComposite _ _ => none - | .paramDynamicHeadWord name wordOffset => - -- Mirrors `Denote.evalExpr`: calldata variant of - -- `Compiler.CompilationModel.DynamicData.checkedParamDynamicHeadWordHelper`. - -- Reads the word at `data_offset + wordOffset * 32` and fails when the - -- position would read past `calldatasize() - 32` (modular, as in Yul). - match lookupBinding? state.bindings s!"{name}_data_offset" with - | some dataOffset => - let wordPos := (dataOffset + wordOffset * 32) % Compiler.Constants.evmModulus - let bound := - (state.world.calldataSize.val + Compiler.Constants.evmModulus - 32) % - Compiler.Constants.evmModulus - if wordPos > bound then none - else - some (Compiler.Proofs.YulGeneration.calldataloadWord - state.selector state.world.calldata wordPos) - | none => none | .literal n => some (wordNormalize n) | .param name => some (lookupValue state.bindings name) | .immutable name => some (state.immutable name).val @@ -1811,18 +1795,7 @@ private theorem evalExpr_paramDynamicHeadWord (state : RuntimeState) (name : String) (wordOffset : Nat) : - evalExpr fields state (.paramDynamicHeadWord name wordOffset) = - match lookupBinding? state.bindings s!"{name}_data_offset" with - | some dataOffset => - let wordPos := (dataOffset + wordOffset * 32) % Compiler.Constants.evmModulus - let bound := - (state.world.calldataSize.val + Compiler.Constants.evmModulus - 32) % - Compiler.Constants.evmModulus - if wordPos > bound then none - else - some (Compiler.Proofs.YulGeneration.calldataloadWord - state.selector state.world.calldata wordPos) - | none => none := rfl + evalExpr fields state (.paramDynamicHeadWord name wordOffset) = none := rfl private theorem evalExpr_paramDynamicStaticComposite (fields : List Field) @@ -4439,22 +4412,8 @@ mutual (Verity.Core.Uint256.ofNat baseVal) (Verity.Core.Uint256.ofNat exponentVal)).val else none - | .paramDynamicHeadWord name wordOffset => - -- Same semantics as `evalExpr` (no helper involvement): calldata - -- variant of `checkedParamDynamicHeadWordHelper`. - match lookupBinding? state.bindings s!"{name}_data_offset" with - | some dataOffset => - let wordPos := (dataOffset + wordOffset * 32) % Compiler.Constants.evmModulus - let bound := - (state.world.calldataSize.val + Compiler.Constants.evmModulus - 32) % - Compiler.Constants.evmModulus - if wordPos > bound then none - else - some (Compiler.Proofs.YulGeneration.calldataloadWord - state.selector state.world.calldata wordPos) - | none => none | .mulDiv512Down _ _ _ | .mulDiv512Up _ _ _ - | .paramDynamicStaticComposite _ _ + | .paramDynamicHeadWord _ _ | .paramDynamicStaticComposite _ _ | .paramDynamicMemberLength _ _ | .paramDynamicMemberDataOffset _ _ | .paramDynamicMemberElement _ _ _ | .arrayElementWord _ _ _ _ diff --git a/Compiler/SoliditySlice/Import.lean b/Compiler/SoliditySlice/Import.lean index 4225c40630..68e69af9de 100644 --- a/Compiler/SoliditySlice/Import.lean +++ b/Compiler/SoliditySlice/Import.lean @@ -18,7 +18,7 @@ open Lean Meta Elab Command namespace Compiler.CompilationModel.SoliditySlice -def importerVersion : String := "solidity-slice-1" +def importerVersion : String := "solidity-slice-2" private def solcLongVersion := "0.8.34+commit.80d5c536" @@ -117,9 +117,9 @@ private structure MemParam where members : Array (String × String) private structure FieldInfo where - name : String slot : Nat term : String + keyCount : Nat memberNames : Array String opaqueNames : Array String @@ -135,7 +135,6 @@ private structure ProjRec where structName : String headWord : Nat modelParam : String - typeString : String private structure OpaqRec where field : String @@ -147,11 +146,8 @@ private structure OpaqRec where private structure SrcParam where name : String id : Nat - typeString : String - location : String private structure Env where - nodes : RBMap Nat Json compare nodeFile : RBMap Nat String compare files : RBMap String ByteArray compare funs : RBMap Nat Json compare @@ -162,6 +158,8 @@ private structure Env where paths : RBMap Nat SPath compare mems : RBMap Nat MemParam compare fieldsByName : RBMap String FieldInfo compare + layoutItems : RBMap String Json compare + layoutTypes : Json included : Array FnRec projections : Array ProjRec opaqueMembers : Array OpaqRec @@ -173,7 +171,6 @@ private structure Env where currentFile : String private def Env.init : Env where - nodes := RBMap.empty nodeFile := RBMap.empty files := RBMap.empty funs := RBMap.empty @@ -184,6 +181,8 @@ private def Env.init : Env where paths := RBMap.empty mems := RBMap.empty fieldsByName := RBMap.empty + layoutItems := RBMap.empty + layoutTypes := Json.null included := #[] projections := #[] opaqueMembers := #[] @@ -194,7 +193,7 @@ private def Env.init : Env where yulNames := RBMap.empty currentFile := "" -abbrev M := StateT Env MetaM +private abbrev M := StateT Env MetaM private def mField (j : Json) (key : String) : M Json := liftM (field j key) private def mStr (j : Json) : M String := liftM (str j) @@ -238,7 +237,9 @@ private def refInt (j : Json) : M Int := do private def fresh : M String := do let n := (← get).next modify fun e => { e with next := e.next + 1 } - pure s!"sliceTmp{n}" + -- A dot cannot occur in a Solidity identifier; generated bindings cannot + -- capture a source local, parameter, or projected parameter. + pure s!"slice.tmp.{n}" private def isAtom (expr : String) : Bool := expr.startsWith "(Compiler.CompilationModel.Expr.literal " || @@ -299,6 +300,87 @@ private def noteProjection (p : ProjRec) : M Unit := do unless env.projections.any (fun q => q.parameter == p.parameter && q.member == p.member) do modify fun e => { e with projections := e.projections.push p } +private def keySyntax (ty : String) : MetaM String := + match ty with + | "t_bytes32" => pure "Compiler.CompilationModel.MappingKeyType.bytes32" + | "t_address" => pure "Compiler.CompilationModel.MappingKeyType.address" + | "t_uint256" => pure "Compiler.CompilationModel.MappingKeyType.uint256" + | _ => throwError "unsupported mapping key {ty}" + +private def layoutType (types : Json) (id : String) : MetaM Json := + field types id + +private def buildField (types item : Json) : M FieldInfo := do + let name ← mStr (← mField item "label") + let slot ← mNat (← mField item "slot") + let typeId ← mStr (← mField item "type") + let top ← liftM (layoutType types typeId) + unless (← mStr (← mField top "encoding")) == "mapping" do + throwError "storage field {name} is not a mapping" + let key1 ← liftM (keySyntax (← mStr (← mField top "key"))) + let valueId ← mStr (← mField top "value") + let value ← liftM (layoutType types valueId) + let encoding ← mStr (← mField value "encoding") + let (key2, structTy) ← + if encoding == "mapping" then + let key2 ← liftM (keySyntax (← mStr (← mField value "key"))) + let innerId ← mStr (← mField value "value") + pure (some key2, ← liftM (layoutType types innerId)) + else if encoding == "inplace" then + pure (none, value) + else + throwError "unsupported mapping value encoding {encoding} for {name}" + let members ← mArr (← mField structTy "members") + let mut terms : Array String := #[] + let mut names : Array String := #[] + let mut skipped : Array String := #[] + for m in members do + let label ← mStr (← mField m "label") + let solcType ← mStr (← mField m "type") + let word ← mNat (← mField m "slot") + let byteOff ← mNat (← mField m "offset") + if solcType.startsWith "t_array" || solcType.startsWith "t_mapping" then + skipped := skipped.push label + modify fun e => + let item : OpaqRec := ⟨name, label, solcType, word, byteOff⟩ + { e with opaqueMembers := e.opaqueMembers.push item } + else if solcType.startsWith "t_uint" then + let some bits := (solcType.drop 6).toNat? | throwError "bad uint {solcType}" + let bitOff := byteOff * 8 + unless bitOff + bits ≤ 256 do throwError "packed field {label} does not fit in a word" + let packed := + if bits == 256 && bitOff == 0 then "none" + else + s!"some ({ "{ " }offset := {bitOff}, width := {bits}{ " }" } : Compiler.CompilationModel.PackedBits)" + let sty := + if bits == 16 then "Compiler.CompilationModel.StructMemberType.uint16" + else "Compiler.CompilationModel.StructMemberType.uint256" + terms := terms.push + s!"({ "{ " }name := {leanStr label}, ty := {sty}, wordOffset := {word}, packed := {packed}{ " }" } : Compiler.CompilationModel.StructMember)" + names := names.push label + else + throwError "unsupported layout member {label} : {solcType}" + let tyExpr := + match key2 with + | some k2 => + s!"Compiler.CompilationModel.FieldType.mappingStruct2 {key1} {k2} {leanList terms}" + | none => + s!"Compiler.CompilationModel.FieldType.mappingStruct {key1} {leanList terms}" + let term := + s!"({ "{ " }name := {leanStr name}, ty := {tyExpr}, slot := some {slot}{ " }" } : Compiler.CompilationModel.Field)" + pure { slot, term, keyCount := if key2.isSome then 2 else 1, + memberNames := names, opaqueNames := skipped } + +/-- Resolve only reached fields: an unrelated unsupported layout must not +prevent importing a supported function closure. -/ +private def resolveField (name : String) (at_ : Json) : M FieldInfo := do + if let some info := (← get).fieldsByName.find? name then return info + let env ← get + let some item := env.layoutItems.find? name | failAt at_ s!"no storage layout for {name}" + let info ← buildField env.layoutTypes item + modify fun e => { e with fieldsByName := e.fieldsByName.insert name info } + return info + private def memberRead (pre : Array String) (path : SPath) (member : String) (at_ : Json) : M Val := do let (fieldName, exprBody) ← match path with | .one field key => pure (field, s!"structMember {leanStr field} {key} {leanStr member}") @@ -393,7 +475,11 @@ private partial def lowerRef (j : Json) : M Ref := do else if env.mems.contains n then pure (.mem n #[]) else if let some decl := env.stateVars.find? n then - pure (.state (← mStr (← mField decl "name")) #[]) + let name ← mStr (← mField decl "name") + if let some item := env.layoutItems.find? name then + unless (← mNat (← mField item "astId")) == n do + failAt j s!"shadowed storage declaration {name} is outside this slice" + pure (.state name #[]) else failAt j s!"unresolved identifier {id}" | "IndexAccess" => @@ -401,10 +487,9 @@ private partial def lowerRef (j : Json) : M Ref := do let key ← atom (← lowerExpr (← mField j "indexExpression")) match base with | .state name pre => - let some info := (← get).fieldsByName.find? name - | failAt j s!"no storage layout for {name}" + let info ← resolveField name j markField name - if info.term.contains "mappingStruct2" then + if info.keyCount == 2 then pure (.path (pre ++ key.pre) (.outer name key.expr)) else pure (.path (pre ++ key.pre) (.one name key.expr)) @@ -415,7 +500,8 @@ private partial def lowerRef (j : Json) : M Ref := do let member ← mStr (← mField j "memberName") let base ← mField j "expression" if member == "timestamp" && (← mKind base) == "Identifier" && - optStr base "name" == some "block" then + optStr base "name" == some "block" && + optStr ((field? base "typeDescriptions").getD Json.null) "typeIdentifier" == some "t_magic_block" then pure (.expr { pre := #[], expr := ex "blockTimestamp" }) else if member == "max" then lowerTypeMax j base @@ -459,7 +545,7 @@ private partial def projectMember (id : Nat) (pre : Array String) (member : Stri let modelParam := s!"{mem.param}_{member}" noteProjection { parameter := mem.param, member := member, structName := mem.structName, - headWord := head, modelParam := modelParam, typeString := mem.members[idx]!.2 } + headWord := head, modelParam := modelParam } pure { pre, expr := ex s!"param {leanStr modelParam}" } private partial def lowerBinary (j : Json) : M Val := do @@ -467,6 +553,8 @@ private partial def lowerBinary (j : Json) : M Val := do let left ← lowerExpr (← mField j "leftExpression") let right ← lowerExpr (← mField j "rightExpression") let common ← mStr (← mField (← mField j "commonType") "typeString") + unless (bitsOf common).isSome || common == "bool" do + failAt j s!"unsupported operand type {common}" match op with | "+" => let some bits := bitsOf common | failAt j s!"unsupported add type {common}" @@ -519,13 +607,13 @@ private partial def checkedMul (bits : Nat) (left right : Val) : M Val := do let dest ← fresh let prod := bin "mul" a.expr b.expr let ok := letBind dest prod - if bits == 256 then - let inner := iteStmt (bin "eq" (bin "div" prod a.expr) b.expr) ok overflowPanic - let ite := iteStmt (bin "eq" a.expr (lit 0)) ok inner - pure { pre := a.pre ++ b.pre |>.push ite, expr := localName dest } - else - let cond := bin "lt" (lit (2 ^ bits - 1)) prod - pure { pre := a.pre ++ b.pre |>.push (iteStmt cond overflowPanic ok), expr := localName dest } + -- Check the EVM-width product before the Solidity-width bound. For e.g. + -- uint248, an overflowing 256-bit product can wrap below the uint248 bound. + let bounded := if bits == 256 then ok + else iteStmt (bin "lt" (lit (2 ^ bits - 1)) prod) overflowPanic ok + let inner := iteStmt (bin "eq" (bin "div" prod a.expr) b.expr) bounded overflowPanic + let ite := iteStmt (bin "eq" a.expr (lit 0)) bounded inner + pure { pre := a.pre ++ b.pre |>.push ite, expr := localName dest } private partial def cmp (op : String) (left right : Val) : M Val := do let a ← atom left @@ -565,6 +653,8 @@ private partial def lowerCast (j : Json) : M Val := do pure v private partial def lowerCall (j : Json) : M Val := do + let names ← mArr (← mField j "names") + unless names.isEmpty do failAt j "named call arguments are outside this slice" let kind ← mStr (← mField j "kind") if kind == "typeConversion" then lowerCast j @@ -598,6 +688,8 @@ private partial def lowerCall (j : Json) : M Val := do private partial def inlineFn (fnId : Nat) (args : Array Val) (at_ : Json) : M Val := do if (← get).stack.contains fnId then failAt at_ s!"recursive call {fnId}" let some fn := (← get).funs.find? fnId | failAt at_ s!"unresolved function {fnId}" + if (field? fn "virtual").bind (fun v => v.getBool?.toOption) == some true then + failAt fn "virtual dispatch is outside this slice" let mods ← mArr (← mField fn "modifiers") unless mods.isEmpty do failAt fn "modifiers are outside this slice" let params ← mArr (← mField (← mField fn "parameters") "parameters") @@ -606,7 +698,8 @@ private partial def inlineFn (fnId : Nat) (args : Array Val) (at_ : Json) : M Va let rets ← mArr (← mField (← mField fn "returnParameters") "parameters") unless rets.size == 1 do failAt fn "only single-value helpers are inlined" let retName ← mStr (← mField rets[0]! "name") - let savedYul := (← get).yulNames + let saved := ← get + let savedYul := saved.yulNames let savedFile := (← get).currentFile let frameFile := (← get).nodeFile.find? fnId |>.getD (← get).currentFile let frame := fnId :: (← get).stack @@ -632,7 +725,9 @@ private partial def inlineFn (fnId : Nat) (args : Array Val) (at_ : Json) : M Va let body ← mField fn "body" let stmts ← mArr (← mField body "statements") let result ← lowerHelper stmts retName - modify fun e => { e with stack := e.stack.tail!, yulNames := savedYul, currentFile := savedFile } + modify fun e => + { e with stack := saved.stack, yulNames := savedYul, currentFile := savedFile, + values := saved.values, paths := saved.paths, mems := saved.mems } pure { pre := pre ++ result.pre, expr := result.expr } private partial def argumentAt (call : Json) (i : Nat) : M Json := do @@ -686,7 +781,7 @@ private partial def lowerAssembly (j : Json) (retName : String) : M Val := do let vars ← mArr (← mField asg "variableNames") unless vars.size == 1 do failAt asg "Yul assignment must have one target" let vname ← mStr (← mField vars[0]! "name") - unless retName == "" || vname == retName do + unless retName != "" && vname == retName do failAt asg s!"Yul assigns {vname}, not the return name {retName}" pure { pre := #[], expr := ← lowerYul (← mField asg "value") } @@ -709,11 +804,11 @@ private partial def lowerLocal (s : Json) : M (Array String) := do | _ => failAt s "storage local is not a resolved read path" else let v ← lowerExpr init - modify fun e => { e with values := e.values.insert id (ex s!"localVar {leanStr name}") } - let env ← get - if env.yulNames.contains name then - modify fun e => { e with yulNames := e.yulNames.insert name (ex s!"localVar {leanStr name}") } - pure (v.pre.push (st s!"letVar {leanStr name} {v.expr}")) + let binding ← fresh + let expr := localName binding + modify fun e => + { e with values := e.values.insert id expr, yulNames := e.yulNames.insert name expr } + pure (v.pre.push (letBind binding v.expr)) end @@ -733,7 +828,7 @@ private def bindRoot (fn : Json) : M (Array SrcParam) := do let id ← mNat (← mField p "id") let ty ← mType p let loc := optStr p "storageLocation" |>.getD "default" - out := out.push { name, id, typeString := ty, location := loc } + out := out.push { name, id } if ty.startsWith "struct " && (loc == "memory" || loc == "calldata") then let typeName ← mField p "typeName" let sid ← refInt typeName @@ -756,16 +851,21 @@ private def bindRoot (fn : Json) : M (Array SrcParam) := do private def lowerRoot (fn : Json) : M (Array String × Array SrcParam) := do let srcParams ← bindRoot fn + if (field? fn "virtual").bind (fun v => v.getBool?.toOption) == some true then + failAt fn "virtual dispatch is outside this slice" let mods ← mArr (← mField fn "modifiers") unless mods.isEmpty do failAt fn "modifiers are outside this slice" let some _ := field? fn "body" | failAt fn "function has no body" let stmts ← mArr (← mField (← mField fn "body") "statements") let mut out : Array String := #[] + let mut returned := false for s in stmts do + if returned then failAt s "statement after root return" match ← mKind s with | "VariableDeclarationStatement" => out := out ++ (← lowerLocal s) | "Return" => + returned := true let expr ← mField s "expression" if (← mKind expr) == "TupleExpression" then if ← mBool (← mField expr "isInlineArray") then @@ -783,86 +883,16 @@ private def lowerRoot (fn : Json) : M (Array String × Array SrcParam) := do let v ← atom (← lowerExpr expr) out := out ++ v.pre |>.push (st s!"returnValues {leanList #[v.expr]}") | kind => failAt s s!"unsupported statement {kind}" + unless returned do failAt fn "an explicit root return is required" pure (out, srcParams) -private def keySyntax (ty : String) : MetaM String := - match ty with - | "t_bytes32" => pure "Compiler.CompilationModel.MappingKeyType.bytes32" - | "t_address" => pure "Compiler.CompilationModel.MappingKeyType.address" - | "t_uint256" => pure "Compiler.CompilationModel.MappingKeyType.uint256" - | _ => throwError "unsupported mapping key {ty}" - -private def layoutType (types : Json) (id : String) : MetaM Json := - field types id - -private def buildField (types item : Json) : M FieldInfo := do - let name ← mStr (← mField item "label") - let slot ← mNat (← mField item "slot") - let typeId ← mStr (← mField item "type") - let top ← liftM (layoutType types typeId) - unless (← mStr (← mField top "encoding")) == "mapping" do - throwError "storage field {name} is not a mapping" - let key1 ← liftM (keySyntax (← mStr (← mField top "key"))) - let valueId ← mStr (← mField top "value") - let value ← liftM (layoutType types valueId) - let encoding ← mStr (← mField value "encoding") - let (ctor, key2, structTy) ← - if encoding == "mapping" then - let key2 ← liftM (keySyntax (← mStr (← mField value "key"))) - let innerId ← mStr (← mField value "value") - pure ("mappingStruct2", some key2, ← liftM (layoutType types innerId)) - else if encoding == "inplace" then - pure ("mappingStruct", none, value) - else - throwError "unsupported mapping value encoding {encoding} for {name}" - let members ← mArr (← mField structTy "members") - let mut terms : Array String := #[] - let mut names : Array String := #[] - let mut skipped : Array String := #[] - for m in members do - let label ← mStr (← mField m "label") - let solcType ← mStr (← mField m "type") - let word ← mNat (← mField m "slot") - let byteOff ← mNat (← mField m "offset") - if solcType.startsWith "t_array" || solcType.startsWith "t_mapping" then - skipped := skipped.push label - modify fun e => - let item : OpaqRec := ⟨name, label, solcType, word, byteOff⟩ - { e with opaqueMembers := e.opaqueMembers.push item } - else if solcType.startsWith "t_uint" then - let some bits := (solcType.drop 6).toNat? | throwError "bad uint {solcType}" - let bitOff := byteOff * 8 - unless bitOff + bits ≤ 256 do throwError "packed field {label} does not fit in a word" - let packed := - if bits == 256 && bitOff == 0 then "none" - else - s!"some ({ "{ " }offset := {bitOff}, width := {bits}{ " }" } : Compiler.CompilationModel.PackedBits)" - let sty := - if bits == 16 then "Compiler.CompilationModel.StructMemberType.uint16" - else "Compiler.CompilationModel.StructMemberType.uint256" - terms := terms.push - s!"({ "{ " }name := {leanStr label}, ty := {sty}, wordOffset := {word}, packed := {packed}{ " }" } : Compiler.CompilationModel.StructMember)" - names := names.push label - else - throwError "unsupported layout member {label} : {solcType}" - let tyExpr := - match key2 with - | some k2 => - s!"Compiler.CompilationModel.FieldType.mappingStruct2 {key1} {k2} {leanList terms}" - | none => - s!"Compiler.CompilationModel.FieldType.mappingStruct {key1} {leanList terms}" - let _ := ctor - let term := - s!"({ "{ " }name := {leanStr name}, ty := {tyExpr}, slot := some {slot}{ " }" } : Compiler.CompilationModel.Field)" - pure { name, slot, term, memberNames := names, opaqueNames := skipped } - private partial def index (file : String) (contract? : Option String) (j : Json) : M Unit := do match j with | .obj o => if (field? j "nodeType").isSome then if let some idj := field? j "id" then let id ← mNat idj - modify fun e => { e with nodes := e.nodes.insert id j, nodeFile := e.nodeFile.insert id file } + modify fun e => { e with nodeFile := e.nodeFile.insert id file } match ← mKind j with | "FunctionDefinition" => modify fun e => @@ -957,7 +987,7 @@ private partial def collectSources | .ok next => acc ← collectSources root next remaps acc pure acc -private def verifyCompiler (compiler : System.FilePath) : MetaM Unit := do +private def verifyCompiler (compiler : System.FilePath) : MetaM String := do let output ← if System.Platform.isOSX then IO.Process.output { cmd := "/usr/bin/shasum", args := #["-a", "256", compiler.toString] } @@ -965,6 +995,7 @@ private def verifyCompiler (compiler : System.FilePath) : MetaM Unit := do IO.Process.output { cmd := "/usr/bin/sha256sum", args := #[compiler.toString] } unless output.exitCode == 0 && officialSolcSha256s.contains (output.stdout.take 64).toString do throwError "compiler checksum mismatch" + return (output.stdout.take 64).toString /-- Verity itself stores the importer next to its lakefile. A downstream package stores that checkout at `.lake/packages/verity`. The digest must hash those @@ -1017,12 +1048,12 @@ private def importSlice (paramTys : Array String) (viaIR optimizer : Bool) (evm bytecodeHash : String) (runs : Nat) : MetaM (String × String × String) := do let compiler := pkgRoot / ".lake/solidity-import/solc-0.8.34" - verifyCompiler compiler + let solcSha ← verifyCompiler compiler let versionOut ← IO.Process.output { cmd := compiler.toString, args := #["--version"] } unless versionOut.exitCode == 0 && acceptedSolcBanners.contains versionOut.stdout.trimAscii.toString do throwError "compiler version mismatch" - verifyCompiler compiler + unless (← verifyCompiler compiler) == solcSha do throwError "compiler changed during import" let remaps ← readRemappings projectRoot let sources ← collectSources projectRoot entry remaps RBMap.empty let mut fileBytes : RBMap String ByteArray compare := RBMap.empty @@ -1047,7 +1078,7 @@ private def importSlice { cmd := compiler.toString, args := #["--standard-json", "--no-import-callback"] } (some input.compress) unless output.exitCode == 0 do throwError "solc failed: {output.stderr}" - verifyCompiler compiler + unless (← verifyCompiler compiler) == solcSha do throwError "compiler changed during import" let parsed ← match Json.parse output.stdout with | .ok v => pure v | .error e => throwError "solc output is not JSON: {e}" @@ -1069,28 +1100,16 @@ private def importSlice let layout ← field chosen "storageLayout" let types ← field layout "types" let items ← arr (← field layout "storage") + env := { env with layoutTypes := types } for item in items do let label ← str (← field item "label") - let typeId ← str (← field item "type") - let top ← layoutType types typeId - let encoding ← str (← field top "encoding") - if encoding == "mapping" then - let value ← layoutType types (← str (← field top "value")) - let valueEnc ← str (← field value "encoding") - let leaf ← - if valueEnc == "mapping" then - layoutType types (← str (← field value "value")) - else - pure value - if (field? leaf "members").isSome then - let (info, env') ← (buildField types item).run env - env := env' - env := { env with fieldsByName := env.fieldsByName.insert label info } + env := { env with layoutItems := env.layoutItems.insert label item } let fn ← (selectFunction contract functionName paramTys).run' env env := { env with currentFile := entry } let ((body, srcParams), env2) ← (lowerRoot fn).run env env := env2 let mut modelParams : Array String := #[] + let mut modelParamNames : Array String := #[] for p in srcParams do if env.mems.contains p.id then let some mem := env.mems.find? p.id | throwError "missing memory parameter {p.name}" @@ -1099,12 +1118,17 @@ private def importSlice if let some proj := env.projections.find? (fun q => q.parameter == p.name && q.member == member) then let some tySyn := paramTypeSyntax ty | throwError "unsupported projected type {ty}" + if modelParamNames.contains proj.modelParam then + throwError "projected parameter name collision: {proj.modelParam}" + modelParamNames := modelParamNames.push proj.modelParam modelParams := modelParams.push s!"({ "{ " }name := {leanStr proj.modelParam}, ty := {tySyn}{ " }" } : Compiler.CompilationModel.Param)" seen := true unless seen do throwError "struct parameter {p.name} was not read" else let some tySyn := env.scalarTy.find? p.id | throwError "missing parameter type" + if modelParamNames.contains p.name then throwError "projected parameter name collision: {p.name}" + modelParamNames := modelParamNames.push p.name modelParams := modelParams.push s!"({ "{ " }name := {leanStr p.name}, ty := {tySyn}{ " }" } : Compiler.CompilationModel.Param)" let retSyntax ← (do @@ -1144,7 +1168,8 @@ private def importSlice let ignored := match env.mems.toList.find? (fun pair => pair.2.param == proj.parameter) with | some (_, mem) => mem.members.filterMap fun pair => - if pair.1 == proj.member then none else some (leanStr pair.1) + if env.projections.any (fun q => q.parameter == proj.parameter && q.member == pair.1) + then none else some (leanStr pair.1) | none => #[] projTerms := projTerms.push s!"({ "{ " }parameter := {leanStr proj.parameter}, member := {leanStr proj.member}, structName := {leanStr proj.structName}, headWord := {proj.headWord}, modelParam := {leanStr proj.modelParam}, ignoredMembers := {leanList ignored}{ " }" } : Compiler.CompilationModel.SoliditySlice.SliceProjection)" @@ -1157,22 +1182,25 @@ private def importSlice let importer ← moduleText sourceRoot "Compiler/SoliditySlice/Import.lean" let coverage ← moduleText sourceRoot "Compiler/SoliditySlice/Coverage.lean" let reportSrc ← moduleText sourceRoot "Compiler/SoliditySlice/Report.lean" - let mut digestInput := s!"{importerVersion}\n{solcLongVersion}\n{contract}\n{functionName}\n" - for ty in paramTys do - digestInput := digestInput ++ ty ++ "\n" - digestInput := digestInput ++ settings.compress ++ "\n" - for (logical, text) in sources.toList.mergeSort (fun a b => a.1 < b.1) do - digestInput := digestInput ++ logical ++ "\n" ++ text ++ "\n" - digestInput := digestInput ++ importer ++ "\n" ++ coverage ++ "\n" ++ reportSrc - let digest := sha256Hex digestInput.toUTF8 - let solcSha := - if System.Platform.isOSX then officialSolcSha256s[1]! else officialSolcSha256s[0]! + -- JSON framing prevents distinct file/signature lists from sharing a + -- concatenation merely because their text contains separator newlines. + let digestInput := Json.mkObj [ + ("importerVersion", Json.str importerVersion), + ("solcVersion", Json.str solcLongVersion), + ("contract", Json.str contract), + ("function", Json.str functionName), + ("parameterTypes", Json.arr (paramTys.map Json.str)), + ("solcInput", input), + ("importerSources", Json.mkObj [ + ("Import.lean", Json.str importer), ("Coverage.lean", Json.str coverage), + ("Report.lean", Json.str reportSrc)])] + let digest := sha256Hex digestInput.compress.toUTF8 let reportTerm := s!"({ "{ " }importerVersion := {leanStr importerVersion}, solcLongVersion := {leanStr solcLongVersion}, solcSha256 := {leanStr solcSha}, settingsJson := {leanStr settings.compress}, sourceDigest := {leanStr digest}, contract := {leanStr contract}, rootFunction := {leanStr functionName}, includedFunctions := {leanList (env.included.map renderFn)}, excludedFunctions := {leanList (sortedExcluded.map renderFn)}, projections := {leanList projTerms}, storageFields := {leanList (env.referenced.map leanStr)}, opaqueMembers := {leanList opaqueTerms}, observesPanicPayload := false{ " }" } : Compiler.CompilationModel.SoliditySlice.SliceReport)" pure (modelTerm, reportTerm, digest) private def elabDef (name : Name) (type body : String) : CommandElabM Unit := do - let cmd := s!"def {name} : {type} :=\n{body}" + let cmd := s!"def _root_.{name} : {type} :=\n{body}" match Parser.runParserCategory (← getEnv) `command cmd "" with | .error e => throwError e | .ok stx => elabCommand stx @@ -1212,7 +1240,7 @@ def elabSliceImport : CommandElab := fun stx => do let some parent := pkg.parent | throwError "package root not found" if parent == pkg then throwError "package root not found" pkg := parent - let project := if ("/" ++ rootArg).startsWith "//" || rootArg.startsWith "/" then + let project := if rootArg.startsWith "/" then System.FilePath.mk rootArg else pkg / rootArg let (modelTerm, reportTerm, digest) ← liftTermElabM <| importSlice pkg project entryPath contractName functionName paramTys useIR optimize evmVersion hashMode runCount @@ -1220,7 +1248,7 @@ def elabSliceImport : CommandElab := fun stx => do elabDef (ns ++ `model) "Compiler.CompilationModel.CompilationModel" modelTerm elabDef (ns ++ `report) "Compiler.CompilationModel.SoliditySlice.SliceReport" reportTerm elabDef (ns ++ `sourceDigest) "String" (leanStr digest) - let covered := s!"theorem {ns ++ `sliceCovered} : Compiler.CompilationModel.SoliditySlice.modelSliceCovered {ns ++ `model} = true := by decide" + let covered := s!"theorem _root_.{ns ++ `sliceCovered} : Compiler.CompilationModel.SoliditySlice.modelSliceCovered {ns ++ `model} = true := by decide" match Parser.runParserCategory (← getEnv) `command covered "" with | .error e => throwError e | .ok stx => elabCommand stx diff --git a/Compiler/SoliditySlice/README.md b/Compiler/SoliditySlice/README.md new file mode 100644 index 0000000000..9e59646ef4 --- /dev/null +++ b/Compiler/SoliditySlice/README.md @@ -0,0 +1,85 @@ +# Solidity function slices + +`solidity_slice_import` elaborates a selected Solidity function and its reached +helpers into the ordinary Verity `CompilationModel`. It consumes a checksum-pinned +solc 0.8.34 AST and storage layout. There is no separate slice interpreter. + +```lean +import Compiler.SoliditySlice.Import + +solidity_slice_import example + slice_root "Contracts/SoliditySliceSmoke" slice_entry "Slice.sol" + slice_contract "C" slice_function "f" + slice_param_tys ["struct Mkt", "bytes32", "address"] + slice_solc "0.8.34+commit.80d5c536" slice_via_ir true slice_evm "osaka" + slice_optimizer true slice_runs 466 slice_bytecode_hash "none" +``` + +The command defines `example.model`, `example.report`, `example.sourceDigest`, +and the kernel theorem `example.sliceCovered`. It also works inside namespaces. +`example.report.toText` renders the included/excluded signatures, projections, +opaque members, settings, and source digest. + +Install the compiler with `python3 scripts/setup_solc_slice.py`; a downstream +package can pass `--output .lake/solidity-import/solc-0.8.34`. Elaboration never +downloads a compiler. `slice_root` resolves relative to the importing package's +`lakefile.lean`, and imports use the project's `remappings.txt`. + +## Supported boundary + +This is deliberately the subset needed by Midnight's `updatePositionView`, +not a claim to support arbitrary Solidity. + +| Construct | Lowering | +| --- | --- | +| Explicit scalar/tuple return | `returnValues`, preserving order | +| Local declarations and storage aliases | Hygienic scalar bindings or resolved read paths | +| One/two-key mappings to structs | solc slots, word offsets, and packed uint offsets | +| Read scalar member of a memory/calldata struct parameter | Explicit scalar projection; no public ABI decoder | +| Unsigned `+`, `-`, `*`, `/` | Word arithmetic with overflow/underflow/zero-divisor guards | +| Unsigned comparisons, equality | Scalar conditions | +| Narrowing casts | Bit masks, not overflow checks | +| Ternaries | Lazy `ite` branches | +| Resolved acyclic helper calls | Inlined bodies with separate local scopes | +| Single assignment to a named assembly return | `xor`, `mul`, `lt` expressions, as in `UtilsLib.min` | + +The root must return explicitly. Loops, state writes, external calls, modifiers, +recursion, virtual dispatch, named call arguments, signed operations, and +unrecognized reached constructs are rejected. Unsupported functions outside the +closure are recorded as excluded. Only reached storage fields are decoded; +opaque array/mapping members are reported and cannot be read. Scalar projections +are checked for parameter-name collisions. + +## Determinism and trust + +Function selection uses the contract, name, and full parameter-type list. Helper +resolution uses solc declaration IDs. Generated names cannot capture Solidity +identifiers; helper-local environments are restored after inlining. Field and +function ordering is deterministic. The digest uses framed JSON containing the +complete solc input, selected signature, release identity, and importer sources. +The report records the checksum actually verified for the invoked compiler. + +A covered constructor has an explicit denotation arm. Coverage is **not** a +proof of source-to-model fidelity, nor membership in the compiler's proven IR +fragment. The translator and solc remain trusted; public ABI decoding, panic +payloads, gas, and bytecode equivalence are outside this interface. + +Lake does not track Solidity files read during elaboration. Consumers must +re-elaborate the import when checking external source freshness, compare the +fresh model/digest with the compiled import, and retain a reviewed inventory. +The Morpho pilot demonstrates that workflow along with universal properties on +`Denote.execStmtList`, rather than properties only on handwritten arithmetic. + +## Validation + +```sh +lake build SoliditySliceSmoke +python3 scripts/solidity_slice_mutations.py +``` + +The regression suite exercises helper/local-name hygiene, namespace use, +shadowed builtins, narrow multiplication overflow, repeated deterministic imports, +unrelated unsupported storage, explicit rejection +of named arguments and implicit returns, reached/unreached unsupported loops, +and detection of changes to arithmetic, field reads, layout, and return order. +The smoke interpreter checks do not replace kernel proofs. diff --git a/Compiler/SoliditySlice/Report.lean b/Compiler/SoliditySlice/Report.lean index c1a44969e6..bb44311489 100644 --- a/Compiler/SoliditySlice/Report.lean +++ b/Compiler/SoliditySlice/Report.lean @@ -56,4 +56,21 @@ structure SliceReport where observesPanicPayload : Bool := false deriving Repr, BEq +/-- Stable, reviewable inventory of the translated closure and explicit exclusions. -/ +def SliceReport.toText (r : SliceReport) : String := Id.run do + let mut lines := [s!"importer {r.importerVersion}", s!"solc {r.solcLongVersion}", + s!"solcSha256 {r.solcSha256}", s!"digest {r.sourceDigest}", + s!"contract {r.contract}", s!"root {r.rootFunction}", s!"settings {r.settingsJson}"] + for fn in r.includedFunctions do + lines := lines ++ [s!"include {fn.contract}.{fn.name} decl {fn.declId} params {fn.paramTypes}"] + for fn in r.excludedFunctions do + lines := lines ++ [s!"exclude {fn.contract}.{fn.name} decl {fn.declId} params {fn.paramTypes}"] + for p in r.projections do + lines := lines ++ [s!"projection {p.parameter}.{p.member} head {p.headWord} as {p.modelParam} ignored {p.ignoredMembers}"] + for name in r.storageFields do + lines := lines ++ [s!"storage {name}"] + for m in r.opaqueMembers do + lines := lines ++ [s!"opaque {m.field}.{m.name} {m.solcType} word {m.wordOffset} byte {m.byteOffset}"] + return String.intercalate "\n" lines ++ "\n" + end Compiler.CompilationModel.SoliditySlice diff --git a/Contracts/SoliditySliceSmoke/Smoke.lean b/Contracts/SoliditySliceSmoke/Smoke.lean index 776edbd49d..05e06db955 100644 --- a/Contracts/SoliditySliceSmoke/Smoke.lean +++ b/Contracts/SoliditySliceSmoke/Smoke.lean @@ -63,7 +63,7 @@ theorem smoke_step_credit : execStmt sliceOracle slice.model.fields (witnessState 100 10 0 0 0 50 100) (peel smokeBody).1 = .continue { witnessState 100 10 0 0 0 50 100 with - bindings := bindValue (witnessBindings 100) "credit" 100 } := by + bindings := bindValue (witnessBindings 100) "slice.tmp.0" 100 } := by dsimp only [peel, smokeBody, slice.model] have hmi : ("m_maturity" == "id") = false := by decide have hmu : ("m_maturity" == "user") = false := by decide @@ -86,13 +86,5 @@ theorem smoke_step_credit : Verity.Core.Uint256.modulus = 100 := by decide simp [hcredit] --- Interpreter checks. Not kernel proofs. `smoke_step_credit` proves the first read. -#eval show IO Unit from do - let success := denoteScalarBody sliceOracle slice.model.fields - (witnessWorld 100 10 0 0 0) 50 (witnessBindings 100) smokeBody - unless success == some [95, 5, 5] do - throw (IO.userError s!"witness A returned {success}") - let reverted := denoteScalarBody sliceOracle slice.model.fields - (witnessWorld 1 2 0 0 0) 1 (witnessBindings 1) smokeBody - unless reverted == none do - throw (IO.userError s!"witness B returned {reverted}") +-- Interpreter witnesses A/B live in scripts/solidity_slice_mutations.py. +-- Keeping test execution out of this proof module satisfies Lean hygiene. diff --git a/TRUST_ASSUMPTIONS.md b/TRUST_ASSUMPTIONS.md index 35782eb841..cbf69a7376 100644 --- a/TRUST_ASSUMPTIONS.md +++ b/TRUST_ASSUMPTIONS.md @@ -35,6 +35,13 @@ Checked `uint256` and narrower unsigned arithmetic is lowered to `Stmt.ite` plus (`bitAnd` with `2^128-1`). Ternaries are `Stmt.ite`, so the untaken branch is not evaluated. Library helpers in an accepted acyclic slice are inlined. `UtilsLib`-style `min` is the Yul term `xor`/`mul`/`lt`, not a renamed `Expr.min`. +Generated scalar bindings are hygienic, and helper-local bindings are scoped. +Only reached storage fields are decoded. Namespace-qualified output names and +framed-JSON provenance are deterministic. Implicit root returns, named call +arguments, virtual dispatch, and unsupported signed operations are rejected. +The slice makes no dynamic ABI-head claim: that unrelated denotation extension +is not part of this change. Lake does not track the external Solidity reads, +so consumers must re-elaborate to verify freshness against a compiled import. ## Proof-only Solidity Vault import diff --git a/Verity/Core/Model/Denote.lean b/Verity/Core/Model/Denote.lean index 48ae69fc63..0e8f1f5900 100644 --- a/Verity/Core/Model/Denote.lean +++ b/Verity/Core/Model/Denote.lean @@ -58,14 +58,11 @@ semantic definition. ## Outside the initial denotation fragment Exactly the constructs `SourceSemantics` itself maps to `none`/`.revert`, apart -from the memory-backed word arrays intentionally widened here and two -extensions landed with the Solidity-slice importer: -`Expr.paramDynamicHeadWord` (dynamic-tuple head-word projection, mirroring the -calldata variant of `checkedParamDynamicHeadWordHelper`) and +from the memory-backed word arrays intentionally widened here and `Stmt.returnValues` (multi-word return recorded in `DenoteState.observedReturnWords`, finished with `.stop`, and surfaced by `denoteFunction` as `DenoteResult.returnWords` in source order). -Still outside: `arrayElementDynamic*`, the remaining `paramDynamic*`, +Still outside: `arrayElementDynamic*`, `paramDynamic*`, `forkIfAtLeast`, `mappingChain` reads with zero or three-plus keys (one/two-key reads and writes are supported), and every `Expr`/`Stmt` constructor not listed @@ -759,22 +756,6 @@ def evalExpr (oracle : DenoteOracle) (fields : List Field) (state : DenoteState) | .paramDynamicMemberDataOffset _ _ => none | .paramDynamicMemberElement _ _ _ => none | .paramDynamicStaticComposite _ _ => none - | .paramDynamicHeadWord name wordOffset => - -- Mirrors the calldata variant of - -- `Compiler.CompilationModel.DynamicData.checkedParamDynamicHeadWordHelper`: - -- read the word at `data_offset + wordOffset * 32`, where `data_offset` - -- is the `{name}_data_offset` binding produced for a dynamic tuple - -- parameter, and fail when the position would read past - -- `calldatasize() - 32`. Arithmetic is modular, as in Yul. - match lookupBinding? state.bindings s!"{name}_data_offset" with - | some dataOffset => - let wordPos := (dataOffset + wordOffset * 32) % Compiler.Constants.evmModulus - let bound := - (state.world.calldataSize.val + Compiler.Constants.evmModulus - 32) % - Compiler.Constants.evmModulus - if wordPos > bound then none - else some (calldataloadWord state.selector state.world.calldata wordPos) - | none => none | .literal n => some (wordNormalize n) | .param name => some (lookupValue state.bindings name) | .immutable name => some (state.immutable name).val diff --git a/artifacts/trust_surface_report.json b/artifacts/trust_surface_report.json index 1d4af2ff19..14fc0b9e80 100644 --- a/artifacts/trust_surface_report.json +++ b/artifacts/trust_surface_report.json @@ -169,7 +169,7 @@ "mechanisms": { "@[implemented_by": 1, "native_decide": 613, - "partial def": 182 + "partial def": 204 }, "notes": "native_decide trusts Lean.ofReduceBool or Lean 4.31 generated per-proof native_decide axioms + Lean.trustCompiler. Prose registry: AXIOMS.md, TRUST_ASSUMPTIONS.md (enforced by scripts/check_trust_surface_registry.py).", "schema_version": 1 diff --git a/scripts/setup_solc_slice.py b/scripts/setup_solc_slice.py index f64499a2c1..cc6318d381 100644 --- a/scripts/setup_solc_slice.py +++ b/scripts/setup_solc_slice.py @@ -8,6 +8,7 @@ from __future__ import annotations +import argparse import hashlib import platform import sys @@ -39,7 +40,7 @@ def file_sha256(path: Path) -> str: return digest.hexdigest() -def install(platform_name: str) -> None: +def install(platform_name: str, dest: Path) -> None: expected = SHA256[platform_name] list_url = f"https://binaries.soliditylang.org/{platform_name}/list.json" request = urllib.request.Request(list_url, headers={"User-Agent": "verity-official-solc/0.8.34"}) @@ -59,8 +60,8 @@ def install(platform_name: str) -> None: if published != expected: raise RuntimeError(f"list.json SHA-256 {published} != pin {expected}") url = f"https://binaries.soliditylang.org/{platform_name}/{match['path']}" - DEST.parent.mkdir(parents=True, exist_ok=True) - tmp = DEST.with_name(DEST.name + ".tmp") + dest.parent.mkdir(parents=True, exist_ok=True) + tmp = dest.with_name(dest.name + ".tmp") try: binary = urllib.request.Request(url, headers={"User-Agent": "verity-official-solc/0.8.34"}) with urllib.request.urlopen(binary, timeout=60) as response, tmp.open("wb") as handle: @@ -68,19 +69,22 @@ def install(platform_name: str) -> None: got = file_sha256(tmp) if got != expected: raise RuntimeError(f"downloaded SHA-256 {got} != {expected}") - tmp.replace(DEST) - DEST.chmod(0o755) + tmp.replace(dest) + dest.chmod(0o755) finally: if tmp.exists(): tmp.unlink() - print(f"installed {LONG_VERSION} ({platform_name}) at {DEST}") + print(f"installed {LONG_VERSION} ({platform_name}) at {dest}") def main() -> int: - if DEST.exists() and file_sha256(DEST) == SHA256[host_platform()]: - print(f"already installed at {DEST}") + parser = argparse.ArgumentParser(description=__doc__) + parser.add_argument("--output", type=Path, default=DEST, help="destination binary path") + dest = parser.parse_args().output + if dest.exists() and file_sha256(dest) == SHA256[host_platform()]: + print(f"already installed at {dest}") return 0 - install(host_platform()) + install(host_platform(), dest) return 0 diff --git a/scripts/solidity_slice_mutations.py b/scripts/solidity_slice_mutations.py index 3dbe04d948..e6afff3867 100644 --- a/scripts/solidity_slice_mutations.py +++ b/scripts/solidity_slice_mutations.py @@ -66,6 +66,10 @@ def body : List Stmt := (witnessWorld 100 10 0 0 0) 50 (witnessBindings 100) body unless success == some [95, 5, 5] do throw (IO.userError s!"witness changed: {success}") + let reverted := denoteScalarBody sliceOracle imported.model.fields + (witnessWorld 1 2 0 0 0) 1 (witnessBindings 1) body + unless reverted == none do + throw (IO.userError s!"revert witness changed: {reverted}") IO.println imported.report.sourceDigest """ @@ -90,12 +94,12 @@ def write_project(name: str, mutate) -> Path: return dest -def expect_success(name: str, project: Path, witness: bool) -> str: +def expect_success(name: str, project: Path, witness: bool, extra: str = "") -> str: lean_path = project / "Check.lean" text = HEADER.format(root=project) if witness: text += WITNESS - lean_path.write_text(text) + lean_path.write_text(text + extra) result = lean(lean_path) if result.returncode != 0: sys.stderr.write(result.stdout) @@ -179,6 +183,70 @@ def swap_layout(dest: Path) -> None: (dest / "Slice.sol").write_text(text) +def helper_local_collision(dest: Path) -> None: + # The caller also has `credit`. Inlining must not overwrite that binding. + path = dest / "Lib.sol" + path.write_text(path.read_text().replace( + "return (x * y) / d;", "uint256 credit = x * y; return credit / d;")) + + +def generated_name_collision(dest: Path) -> None: + path = dest / "Slice.sol" + path.write_text(path.read_text().replace("credit = p.credit", "sliceTmp0 = p.credit") + .replace("credit.mulDivDown", "sliceTmp0.mulDivDown") + .replace("credit > 0", "sliceTmp0 > 0") + .replace("credit - post", "sliceTmp0 - post") + .replace(", credit)", ", sliceTmp0)")) + + +def unrelated_layout(dest: Path) -> None: + path = dest / "Slice.sol" + path.write_text(path.read_text().replace( + "contract C {", "struct Unused { int128 signedValue; }\ncontract C {").replace( + "public marketState;", "public marketState;\n mapping(bytes32 => Unused) private unusedState;")) + + +def named_arguments(dest: Path) -> None: + path = dest / "Lib.sol" + path.write_text(path.read_text().replace( + "return (x * y) / d;", "return named({d: d, y: y, x: x});")) + with path.open("a") as out: + out.write("\nfunction named(uint256 x, uint256 y, uint256 d) pure returns (uint256) { return x*y/d; }\n") + + +def implicit_return(dest: Path) -> None: + path = dest / "Slice.sol" + path.write_text(path.read_text().replace( + "returns (uint128, uint128, uint128)", "returns (uint128 a, uint128 b, uint128 c)").replace( + "return (uint128(post) - fee, uint128(postFee) - fee, fee);", "")) + + +def projected_name_collision(dest: Path) -> None: + path = dest / "Slice.sol" + path.write_text(path.read_text().replace("bytes32 id,", "bytes32 m_maturity,") + .replace("[id]", "[m_maturity]")) + + +def shadow_builtin(dest: Path) -> None: + path = dest / "Slice.sol" + path.write_text(path.read_text().replace("uint256 ignored;", "uint256 timestamp;") + .replace("Mkt memory m,", "Mkt memory block,") + .replace("m.maturity", "block.maturity")) + + +def narrow_multiply(dest: Path) -> None: + (dest / "Slice.sol").write_text("""pragma solidity 0.8.34; +struct Mkt { uint256 maturity; } +contract C { + function f(Mkt memory m, bytes32 id, address user) external pure returns (uint128, uint128, uint128) { + uint248 x = uint248(m.maturity); + uint248 product = x * x; + return (uint128(product), 0, 0); + } +} +""") + + def digest_of(blob: str) -> str: lines = [line.strip() for line in blob.splitlines() if len(line.strip()) == 64] if not lines: @@ -191,6 +259,34 @@ def main() -> None: raise SystemExit(f"missing {SOLC}") WORK.mkdir(parents=True, exist_ok=True) base = expect_success("unreached-for", write_project("base", unchanged), witness=True) + expect_success("helper-local-hygiene", write_project("helper-local", helper_local_collision), witness=True) + expect_success("generated-name-hygiene", write_project("generated-name", generated_name_collision), witness=True) + expect_success("unrelated-layout", write_project("unrelated-layout", unrelated_layout), witness=True) + expect_failure("named-arguments", write_project("named-arguments", named_arguments), "named call arguments") + expect_failure("implicit-return", write_project("implicit-return", implicit_return), "explicit root return") + expect_failure("projection-name-collision", write_project("projection-name", projected_name_collision), "projected parameter name collision") + namespaced = WORK / "base/Namespaced.lean" + namespaced.write_text(HEADER.format(root=WORK / "base").replace( + "solidity_slice_import imported", "namespace Nested\nsolidity_slice_import imported") + + "\nend Nested\nexample : modelSliceCovered Nested.imported.model = true := Nested.imported.sliceCovered\n") + result = lean(namespaced) + if result.returncode: + raise SystemExit(result.stdout + result.stderr) + print("pass namespaced-import") + expect_success("shadowed-block", write_project("shadowed-block", shadow_builtin), witness=False, + extra='\n#eval show IO Unit from do\n unless imported.report.projections.any (fun p => p.member == "timestamp") do\n throw (IO.userError "shadowed block was treated as a builtin")\n') + expect_success("narrow-multiply-overflow", write_project("narrow-multiply", narrow_multiply), witness=False, + extra=r''' +#eval show IO Unit from do + let oracle : DenoteOracle := { mappingSlot := fun _ _ => 0, keccakMemorySlice := fun _ _ _ => 0 } + let body := match imported.model.functions with | f :: _ => f.body | [] => [] + let run := fun maturity => denoteScalarBody oracle imported.model.fields Verity.defaultState 0 [("m_maturity", maturity)] body + unless run 3 == some [9, 0, 0] do throw (IO.userError "small uint248 product changed") + unless run (2^128) == none do throw (IO.userError "uint248 product overflow was accepted") +''') + repeated = expect_success("deterministic", WORK / "base", witness=True) + if digest_of(base) != digest_of(repeated): + raise SystemExit("identical input did not reproduce the digest") rounding = expect_failure("rounding-detected", write_project("rounding", change_rounding), "witness changed") expect_failure("factor-detected", write_project("factor", change_factor), "witness changed") expect_failure("field-detected", write_project("field", change_field), "witness changed") From 38dc5cc3042b7e1609ec5029cd8b3176cf403934 Mon Sep 17 00:00:00 2001 From: Thomas Marchand Date: Wed, 23 Sep 2026 17:01:59 +0200 Subject: [PATCH 07/22] Run Solidity slice mutation regressions in CI --- .github/workflows/verify.yml | 6 ++++++ scripts/verify_sync_spec.json | 7 +++++-- scripts/verify_sync_spec_source.py | 6 ++++-- 3 files changed, 15 insertions(+), 4 deletions(-) diff --git a/.github/workflows/verify.yml b/.github/workflows/verify.yml index bbf17e98ea..0ee0164f40 100644 --- a/.github/workflows/verify.yml +++ b/.github/workflows/verify.yml @@ -424,6 +424,12 @@ jobs: exit "${build_status}" fi + - name: Check Solidity slice imports and mutation regressions + run: | + python3 scripts/setup_solc_slice.py + lake build SoliditySliceSmoke + python3 scripts/solidity_slice_mutations.py + - name: Check storage layout audit artifacts (#1897) run: make verify-storage-layout diff --git a/scripts/verify_sync_spec.json b/scripts/verify_sync_spec.json index a7365ce46f..a46f27c76a 100644 --- a/scripts/verify_sync_spec.json +++ b/scripts/verify_sync_spec.json @@ -824,10 +824,13 @@ ], "expected_checks_other_commands": [], "expected_build_commands": [ - "check_lean_warning_regression.py --log lake-build.log" + "check_lean_warning_regression.py --log lake-build.log", + "setup_solc_slice.py", + "solidity_slice_mutations.py" ], "required_build_run_commands": [ - "lake build PrintAxioms" + "lake build PrintAxioms", + "lake build SoliditySliceSmoke" ], "expected_build_audit_commands": [ "check_split_package_builds.py", diff --git a/scripts/verify_sync_spec_source.py b/scripts/verify_sync_spec_source.py index 4baa0d6244..9f8c59a7ce 100644 --- a/scripts/verify_sync_spec_source.py +++ b/scripts/verify_sync_spec_source.py @@ -706,8 +706,10 @@ 'python3 scripts/check_issue_1060_integrity.py', "python3 -m unittest discover -s scripts -p 'test_*.py' -v"], 'expected_checks_other_commands': [], - 'expected_build_commands': ['check_lean_warning_regression.py --log lake-build.log'], - 'required_build_run_commands': ['lake build PrintAxioms'], + 'expected_build_commands': ['check_lean_warning_regression.py --log lake-build.log', + 'setup_solc_slice.py', + 'solidity_slice_mutations.py'], + 'required_build_run_commands': ['lake build PrintAxioms', 'lake build SoliditySliceSmoke'], 'expected_build_audit_commands': ['check_split_package_builds.py', 'check_axioms.py', 'check_proof_length.py --format=markdown >> ' From c134c2e3d904429ebaf1f054c2f15c9e98fbe311 Mon Sep 17 00:00:00 2001 From: Thomas Marchand Date: Thu, 24 Sep 2026 12:17:46 +0200 Subject: [PATCH 08/22] Validate Solidity slices against Denote and compiled EVM execution --- .github/workflows/verify.yml | 41 ++++ Compiler/CompilationModel/Compile.lean | 10 +- Compiler/Proofs/IRGeneration/Function.lean | 6 +- .../IRGeneration/FunctionBody/Stmt.lean | 18 +- .../GenericInduction/Storage.lean | 2 +- Compiler/SoliditySlice/Differential.lean | 98 ++++++++ Compiler/SoliditySlice/Import.lean | 80 +++--- Compiler/SoliditySlice/README.md | 7 +- Compiler/SoliditySlice/TESTING.md | 115 +++++++++ Contracts/SoliditySliceSmoke/Smoke.lean | 2 +- .../differential-cases.json | 4 + .../SoliditySliceSmoke/differential.json | 63 +++++ scripts/check_solidity_differential.sh | 11 + scripts/solidity_differential/Runner.t.sol | 80 ++++++ scripts/solidity_differential/__init__.py | 1 + scripts/solidity_differential/cases.py | 117 +++++++++ scripts/solidity_differential/engine.py | 232 ++++++++++++++++++ scripts/solidity_differential/mutations.py | 73 ++++++ scripts/solidity_differential/programs.py | 111 +++++++++ scripts/solidity_differential/reduce.py | 80 ++++++ .../solidity_differential/requirements.txt | 2 + scripts/solidity_slice_differential.py | 63 +++++ scripts/solidity_slice_mutations.py | 14 +- scripts/test_solidity_differential.py | 78 ++++++ scripts/verify_sync_spec.json | 16 ++ scripts/verify_sync_spec_source.py | 9 + 26 files changed, 1281 insertions(+), 52 deletions(-) create mode 100644 Compiler/SoliditySlice/Differential.lean create mode 100644 Compiler/SoliditySlice/TESTING.md create mode 100644 Contracts/SoliditySliceSmoke/differential-cases.json create mode 100644 Contracts/SoliditySliceSmoke/differential.json create mode 100755 scripts/check_solidity_differential.sh create mode 100644 scripts/solidity_differential/Runner.t.sol create mode 100644 scripts/solidity_differential/__init__.py create mode 100644 scripts/solidity_differential/cases.py create mode 100644 scripts/solidity_differential/engine.py create mode 100644 scripts/solidity_differential/mutations.py create mode 100644 scripts/solidity_differential/programs.py create mode 100644 scripts/solidity_differential/reduce.py create mode 100644 scripts/solidity_differential/requirements.txt create mode 100755 scripts/solidity_slice_differential.py create mode 100644 scripts/test_solidity_differential.py diff --git a/.github/workflows/verify.yml b/.github/workflows/verify.yml index 0ee0164f40..4bdd3eb50b 100644 --- a/.github/workflows/verify.yml +++ b/.github/workflows/verify.yml @@ -231,6 +231,11 @@ jobs: - 'packages/**' - 'lakefile.lean' - 'lake-manifest.json' + - 'scripts/solidity_differential/**' + - 'scripts/solidity_slice_differential.py' + - 'scripts/check_solidity_differential.sh' + - 'scripts/solidity_slice_mutations.py' + - 'scripts/setup_solc_slice.py' - 'lean-toolchain' compiler: - '.github/actions/**' @@ -255,6 +260,11 @@ jobs: - 'scripts/property_utils.py' - 'lakefile.lean' - 'lake-manifest.json' + - 'scripts/solidity_differential/**' + - 'scripts/solidity_slice_differential.py' + - 'scripts/check_solidity_differential.sh' + - 'scripts/solidity_slice_mutations.py' + - 'scripts/setup_solc_slice.py' - 'lean-toolchain' - 'foundry.toml' - name: Resolve effective change flags @@ -1234,6 +1244,37 @@ jobs: if: steps.local-restore-compiler-workspace.outcome != 'success' run: tar -xf lean-workspace-compiler-build.tar + - name: Install Foundry for Solidity slice comparisons + uses: foundry-rs/foundry-toolchain@v1 + with: + version: v1.5.0 + + - name: Compare Solidity, Denote and compiled models + timeout-minutes: 10 + run: | + scripts/check_solidity_differential.sh --config Contracts/SoliditySliceSmoke/differential.json --output .lake/slice-differential/smoke --cases 128 --seed 2438 + scripts/check_solidity_differential.sh --programs 5 --cases 16 --seed 2438 --output .lake/slice-differential/programs + + - name: Extended Solidity slice campaigns + if: github.event_name == 'schedule' || github.event_name == 'workflow_dispatch' + timeout-minutes: 60 + run: | + scripts/check_solidity_differential.sh --config Contracts/SoliditySliceSmoke/differential.json --output .lake/slice-differential/nightly-inputs --cases 2048 --seed "$GITHUB_RUN_NUMBER" + scripts/check_solidity_differential.sh --programs 20 --cases 64 --seed "$GITHUB_RUN_NUMBER" --output .lake/slice-differential/nightly-programs + scripts/check_solidity_differential.sh --mutations --output .lake/slice-differential/mutants + + - name: Archive Solidity slice comparison evidence + if: always() + uses: actions/upload-artifact@v7 + with: + name: solidity-slice-differential + path: | + .lake/slice-differential/** + !.lake/slice-differential/**/.lake/build/** + !.lake/slice-differential/**/.lake/packages/** + include-hidden-files: true + retention-days: 14 + - name: Build compiler CLI regression executable run: stdbuf -oL -eL lake build compiler-main-test diff --git a/Compiler/CompilationModel/Compile.lean b/Compiler/CompilationModel/Compile.lean index 1160b38054..b8497a4e76 100644 --- a/Compiler/CompilationModel/Compile.lean +++ b/Compiler/CompilationModel/Compile.lean @@ -42,6 +42,14 @@ import Compiler.Yul.StatementRegions namespace Compiler.CompilationModel +/-- Nested if/else blocks cannot redeclare a surrounding Yul local, even in a +new block. Strictly smaller branch subtrees receive different numeric bases; +`pickFreshName` still protects against every source name in scope. -/ +def iteCondBaseName (thenBranch elseBranch : List Stmt) : String := + let count := Stmt.foldList (fun n _ _ => n + 1) 0 (thenBranch ++ elseBranch) + s!"__ite_cond_{count}" + + open Compiler open Compiler.Yul @@ -369,7 +377,7 @@ def compileStmtWithFork (fields : List Field) (events : List EventDef := []) -- Wrapped in block { } and freshened against names seen in this if/else shape -- so user locals cannot shadow the compiler-generated temp. let iteUsedNames := inScopeNames ++ collectExprNames cond ++ collectStmtListNames thenBranch ++ collectStmtListNames elseBranch - let iteCondName := pickFreshName "__ite_cond" iteUsedNames + let iteCondName := pickFreshName (iteCondBaseName thenBranch elseBranch) iteUsedNames pure [YulStmt.block [ YulStmt.let_ iteCondName condExpr, YulStmt.if_ (YulExpr.ident iteCondName) thenStmts, diff --git a/Compiler/Proofs/IRGeneration/Function.lean b/Compiler/Proofs/IRGeneration/Function.lean index c3c49b9d19..2283ad2712 100644 --- a/Compiler/Proofs/IRGeneration/Function.lean +++ b/Compiler/Proofs/IRGeneration/Function.lean @@ -3362,14 +3362,14 @@ theorem stmtListTerminalCore_compiledLegacyCompatible cases hcompile exact .block [YulStmt.let_ - (pickFreshName "__ite_cond" + (pickFreshName (iteCondBaseName thenBranch elseBranch) (compilerScope ++ (collectExprNames cond ++ (collectStmtListNames thenBranch ++ collectStmtListNames elseBranch)))) condIR, YulStmt.if_ (YulExpr.ident - (pickFreshName "__ite_cond" + (pickFreshName (iteCondBaseName thenBranch elseBranch) (compilerScope ++ (collectExprNames cond ++ (collectStmtListNames thenBranch ++ collectStmtListNames elseBranch))))) @@ -3377,7 +3377,7 @@ theorem stmtListTerminalCore_compiledLegacyCompatible YulStmt.if_ (YulExpr.call "iszero" [YulExpr.ident - (pickFreshName "__ite_cond" + (pickFreshName (iteCondBaseName thenBranch elseBranch) (compilerScope ++ (collectExprNames cond ++ (collectStmtListNames thenBranch ++ collectStmtListNames elseBranch))))]) diff --git a/Compiler/Proofs/IRGeneration/FunctionBody/Stmt.lean b/Compiler/Proofs/IRGeneration/FunctionBody/Stmt.lean index dd1b268d18..d562d92661 100644 --- a/Compiler/Proofs/IRGeneration/FunctionBody/Stmt.lean +++ b/Compiler/Proofs/IRGeneration/FunctionBody/Stmt.lean @@ -1534,7 +1534,7 @@ theorem compileStmt_terminal_ite_ok_inv CompilationModel.compileStmtList fields [] [] .calldata [] false inScopeNames [] elseBranch = Except.ok elseIR ∧ tempName = - CompilationModel.pickFreshName "__ite_cond" + CompilationModel.pickFreshName (CompilationModel.iteCondBaseName thenBranch elseBranch) (inScopeNames ++ collectExprNames cond ++ collectStmtListNames thenBranch ++ collectStmtListNames elseBranch) ∧ bodyIR = @@ -1563,7 +1563,7 @@ theorem compileStmt_terminal_ite_ok_inv simp [hcond, compileStmtListWithFork_cancun_eq_compileStmtList, hthen, helse, helseNonempty] at hcompile refine ⟨condIR, thenIR, elseIR, - CompilationModel.pickFreshName "__ite_cond" + CompilationModel.pickFreshName (CompilationModel.iteCondBaseName thenBranch elseBranch) (inScopeNames ++ collectExprNames cond ++ collectStmtListNames thenBranch ++ collectStmtListNames elseBranch), ?_, ?_, ?_, rfl, ?_⟩ @@ -1595,7 +1595,7 @@ theorem compileStmtList_terminal_ite_ok_inv (collectStmtBindNames (.ite cond thenBranch elseBranch) ++ inScopeNames) [] rest = Except.ok tailIR ∧ tempName = - CompilationModel.pickFreshName "__ite_cond" + CompilationModel.pickFreshName (CompilationModel.iteCondBaseName thenBranch elseBranch) (inScopeNames ++ collectExprNames cond ++ collectStmtListNames thenBranch ++ collectStmtListNames elseBranch) ∧ bodyIR = @@ -1770,20 +1770,20 @@ theorem compileStmtList_terminal_core_ok refine ⟨[YulStmt.block [ YulStmt.let_ - (CompilationModel.pickFreshName "__ite_cond" + (CompilationModel.pickFreshName (CompilationModel.iteCondBaseName thenBranch elseBranch) (inScopeNames ++ collectExprNames cond ++ collectStmtListNames thenBranch ++ collectStmtListNames elseBranch)) condIR, YulStmt.if_ (YulExpr.ident - (CompilationModel.pickFreshName "__ite_cond" + (CompilationModel.pickFreshName (CompilationModel.iteCondBaseName thenBranch elseBranch) (inScopeNames ++ collectExprNames cond ++ collectStmtListNames thenBranch ++ collectStmtListNames elseBranch))) thenIR, YulStmt.if_ (YulExpr.call "iszero" [YulExpr.ident - (CompilationModel.pickFreshName "__ite_cond" + (CompilationModel.pickFreshName (CompilationModel.iteCondBaseName thenBranch elseBranch) (inScopeNames ++ collectExprNames cond ++ collectStmtListNames thenBranch ++ collectStmtListNames elseBranch))]) elseIR @@ -7412,7 +7412,7 @@ theorem compiled_terminal_ite_temp_not_mem_scope {cond : Expr} {thenBranch elseBranch : List Stmt} (hincluded : scopeNamesIncluded scope inScopeNames) : - CompilationModel.pickFreshName "__ite_cond" + CompilationModel.pickFreshName (CompilationModel.iteCondBaseName thenBranch elseBranch) (inScopeNames ++ collectExprNames cond ++ collectStmtListNames thenBranch ++ collectStmtListNames elseBranch) ∉ scope := by apply pickFreshName_not_mem_scope_of_subset @@ -7430,7 +7430,7 @@ theorem bindingsExactlyMatchIRVarsOnScope_setCompiledTerminalIteTemp_irrelevant (hincluded : scopeNamesIncluded scope inScopeNames) : bindingsExactlyMatchIRVarsOnScope scope bindings (state.setVar - (CompilationModel.pickFreshName "__ite_cond" + (CompilationModel.pickFreshName (CompilationModel.iteCondBaseName thenBranch elseBranch) (inScopeNames ++ collectExprNames cond ++ collectStmtListNames thenBranch ++ collectStmtListNames elseBranch)) value) := by @@ -7862,7 +7862,7 @@ theorem exec_compileStmtList_terminal_core_sizeOf_extraFuel | nil => exfalso; exact stmtListTerminalCore_ne_nil helse rfl | cons => simp let tempName := - CompilationModel.pickFreshName "__ite_cond" + CompilationModel.pickFreshName (CompilationModel.iteCondBaseName thenBranch elseBranch) (inScopeNames ++ collectExprNames cond ++ collectStmtListNames thenBranch ++ collectStmtListNames elseBranch) refine ⟨[YulStmt.block diff --git a/Compiler/Proofs/IRGeneration/GenericInduction/Storage.lean b/Compiler/Proofs/IRGeneration/GenericInduction/Storage.lean index 862cc336c9..4866e791a9 100644 --- a/Compiler/Proofs/IRGeneration/GenericInduction/Storage.lean +++ b/Compiler/Proofs/IRGeneration/GenericInduction/Storage.lean @@ -7278,7 +7278,7 @@ theorem compiledStmtStep_ite | nil => exfalso; exact FunctionBody.stmtListTerminalCore_ne_nil helse rfl | cons => simp let tempName := - CompilationModel.pickFreshName "__ite_cond" + CompilationModel.pickFreshName (CompilationModel.iteCondBaseName thenBranch elseBranch) (scope ++ collectExprNames cond ++ collectStmtListNames thenBranch ++ collectStmtListNames elseBranch) let compiledIR := diff --git a/Compiler/SoliditySlice/Differential.lean b/Compiler/SoliditySlice/Differential.lean new file mode 100644 index 0000000000..dd1c112e90 --- /dev/null +++ b/Compiler/SoliditySlice/Differential.lean @@ -0,0 +1,98 @@ +import Compiler.SoliditySlice.Coverage +import Compiler.SoliditySlice.Report +import Compiler.CompilationModel +import Compiler.Codegen +import Compiler.Yul.PrettyPrint +import Compiler.Keccak.Sponge +import Lean.Data.Json + +/-! Test-only runner for an arbitrary covered Solidity slice. The execution path +is the same `Denote.execStmtList` used by proofs. JSON/IO and the concrete hash +implementation are harness code, not additional proof assumptions. -/ +namespace Compiler.CompilationModel.SoliditySlice.Differential +open Lean Compiler.CompilationModel.Denote Verity.Core + +private def word (j : Json) : Except String Nat := do + let s ← j.getStr? + let some n := s.toNat? | throw "expected an unsigned decimal word" + unless n < 2^256 do throw "word exceeds uint256" + return n + +private def words (j : Json) : Except String (List Nat) := do + (← j.getArr?).toList.mapM word + +private def pairs (j : Json) : Except String (List (Nat × Nat)) := do + (← j.getArr?).toList.mapM fun row => do + let xs ← words row + match xs with + | [slot, value] => return (slot, value) + | _ => throw "storage row must contain slot and value" + +private def mappingSlot (base key : Nat) : Nat := + let bytes := fun n : Nat => ByteArray.mk ((List.range 32).toArray.map fun i => + UInt8.ofNat (n / 2^(8*(31-i)) % 256)) + (KeccakEngine.keccak256 (bytes key ++ bytes base)).data.foldl + (fun n b => n * 256 + b.toNat) 0 + +private def oracle : DenoteOracle := + { mappingSlot, keccakMemorySlice := fun _ _ _ => 0 } + +private def jsonWords (xs : List Nat) : Json := toJson (xs.map toString) + +private def execute (model : CompilationModel) (fn : FunctionSpec) (j : Json) : Except String Json := do + let ident ← j.getObjValAs? String "id" + let args ← words (← j.getObjVal? "args") + unless args.length == fn.params.length do throw "model argument count mismatch" + let storage ← pairs (← j.getObjVal? "storage") + let slots ← words (← j.getObjVal? "observe") + unless (storage.map Prod.fst).eraseDups.length == storage.length do + throw "duplicate initial storage slot" + let timestamp ← word (← j.getObjVal? "timestamp") + let world := Verity.defaultState.withStorageWords fun slot => + match slot with + | .slot n => Uint256.ofNat ((storage.find? (fun p => p.1 == n)).map Prod.snd |>.getD 0) + | _ => Verity.defaultState.storageWords slot + let initial : DenoteState := + { world := { world with blockTimestamp := Uint256.ofNat timestamp } + bindings := fn.params.map (·.name) |>.zip args } + let result := execStmtList oracle model.fields initial fn.body + let (status, output, finalWorld) ← match result with + | .stop final => match final.observedReturnWords with + | some xs => pure ("ok", xs, final.world) + | none => throw "covered slice stopped without return words" + | .revert => pure ("revert", [], initial.world) + | _ => throw "covered slice did not return or revert" + return Json.mkObj [("id", toJson ident), ("status", toJson status), + ("words", jsonWords output), + ("storage", jsonWords (slots.map fun slot => (finalWorld.storageWords (.slot slot)).val))] + +/-- The driver imports the selected model, then invokes this shared entrypoint. +`describe` also attempts the ordinary Verity compiler; a failure is explicit. -/ +def run (model : CompilationModel) (report : SliceReport) (args : List String) : IO UInt32 := do + unless modelSliceCovered model do throw (IO.userError "unsupported Denote slice") + let [fn] := model.functions | throw (IO.userError "expected exactly one imported root") + match args with + | ["describe", output, yulPath] => + let compiled := Compiler.CompilationModel.compile model [0x12345678] .osaka + let (compilable, reason) ← match compiled with + | .ok ir => + IO.FS.writeFile yulPath (Compiler.Yul.render (Compiler.emitYul ir)) + pure (true, "") + | .error reason => pure (false, reason) + let projections := report.projections.map fun p => Json.mkObj + [("parameter", toJson p.parameter), ("member", toJson p.member), ("modelParam", toJson p.modelParam)] + IO.FS.writeFile output (Json.compress (Json.mkObj + [("params", toJson (fn.params.map (·.name))), ("projections", toJson projections), + ("digest", toJson report.sourceDigest), ("settings", toJson report.settingsJson), + ("compilable", toJson compilable), ("compileError", toJson reason), + ("panicPayloadObserved", toJson report.observesPanicPayload)])) + | ["run", input, output, digest] => + unless digest == report.sourceDigest do throw (IO.userError "source/importer digest changed; rebuild the campaign") + let parsed ← IO.ofExcept (Json.parse (← IO.FS.readFile input)) + let results ← IO.ofExcept do + (← parsed.getArr?).toList.mapM (execute model fn) + IO.FS.writeFile output (Json.compress (toJson results)) + | _ => throw (IO.userError "expected describe OUTPUT YUL or run INPUT OUTPUT DIGEST") + return 0 + +end Compiler.CompilationModel.SoliditySlice.Differential diff --git a/Compiler/SoliditySlice/Import.lean b/Compiler/SoliditySlice/Import.lean index 68e69af9de..7d6e5da787 100644 --- a/Compiler/SoliditySlice/Import.lean +++ b/Compiler/SoliditySlice/Import.lean @@ -18,7 +18,7 @@ open Lean Meta Elab Command namespace Compiler.CompilationModel.SoliditySlice -def importerVersion : String := "solidity-slice-2" +def importerVersion : String := "solidity-slice-3" private def solcLongVersion := "0.8.34+commit.80d5c536" @@ -148,6 +148,7 @@ private structure SrcParam where id : Nat private structure Env where + sourceNames : List String nodeFile : RBMap Nat String compare files : RBMap String ByteArray compare funs : RBMap Nat Json compare @@ -171,6 +172,7 @@ private structure Env where currentFile : String private def Env.init : Env where + sourceNames := [] nodeFile := RBMap.empty files := RBMap.empty funs := RBMap.empty @@ -205,6 +207,11 @@ private def mType (j : Json) : M String := liftM (typeString j) private def failAt (j : Json) (why : String) : M α := do let env ← get + let frames := env.stack.reverse.map fun id => + let owner := env.funContract.find? id |>.getD "" + let name := (env.funs.find? id).bind (fun fn => optStr fn "name") |>.getD s!"decl#{id}" + s!"{owner}.{name}" + let why := s!"[solidity-slice:unsupported] {why}\nclosure: {String.intercalate " -> " frames}" let file := match (field? j "id").bind (fun id => id.getNat?.toOption) with | some id => env.nodeFile.find? id |>.getD env.currentFile @@ -235,11 +242,18 @@ private def refInt (j : Json) : M Int := do | .error e => failAt j e private def fresh : M String := do - let n := (← get).next - modify fun e => { e with next := e.next + 1 } - -- A dot cannot occur in a Solidity identifier; generated bindings cannot - -- capture a source local, parameter, or projected parameter. - pure s!"slice.tmp.{n}" + let env ← get + let projected := env.mems.toList.flatMap fun (_, p) => + p.members.toList.map (fun (member, _) => s!"{p.param}_{member}") + let reserved := env.sourceNames ++ projected + -- The prefix is a valid compiler identifier, but is not assumed reserved by + -- Solidity. Check every source name and every potential scalar projection. + for _ in [:reserved.length + 1] do + let n := (← get).next + modify fun e => { e with next := e.next + 1 } + let candidate := s!"_verity_slice_tmp_{n}" + unless reserved.contains candidate do return candidate + throwError "unable to allocate a hygienic slice binding" private def isAtom (expr : String) : Bool := expr.startsWith "(Compiler.CompilationModel.Expr.literal " || @@ -377,7 +391,8 @@ private def resolveField (name : String) (at_ : Json) : M FieldInfo := do if let some info := (← get).fieldsByName.find? name then return info let env ← get let some item := env.layoutItems.find? name | failAt at_ s!"no storage layout for {name}" - let info ← buildField env.layoutTypes item + let info ← try buildField env.layoutTypes item catch ex => + failAt at_ (← ex.toMessageData.toString) modify fun e => { e with fieldsByName := e.fieldsByName.insert name info } return info @@ -407,6 +422,9 @@ private def localName (name : String) : String := private def letBind (name value : String) : String := st ("letVar " ++ leanStr name ++ " " ++ value) +private def assignBind (name value : String) : String := + st ("assignVar " ++ leanStr name ++ " " ++ value) + private def iteStmt (cond thenStmt elseStmt : String) : String := st ("ite " ++ cond ++ " [" ++ thenStmt ++ "] [" ++ elseStmt ++ "]") @@ -578,42 +596,42 @@ private partial def checkedSub (left right : Val) : M Val := do let a ← atom left let b ← atom right let dest ← fresh - let ok := letBind dest (bin "sub" a.expr b.expr) + let ok := assignBind dest (bin "sub" a.expr b.expr) let ite := iteStmt (bin "lt" a.expr b.expr) overflowPanic ok - pure { pre := a.pre ++ b.pre |>.push ite, expr := localName dest } + pure { pre := a.pre ++ b.pre |>.push (letBind dest (lit 0)) |>.push ite, expr := localName dest } private partial def checkedDiv (left right : Val) : M Val := do let a ← atom left let b ← atom right let dest ← fresh - let ok := letBind dest (bin "div" a.expr b.expr) + let ok := assignBind dest (bin "div" a.expr b.expr) let ite := iteStmt (bin "eq" b.expr (lit 0)) divPanic ok - pure { pre := a.pre ++ b.pre |>.push ite, expr := localName dest } + pure { pre := a.pre ++ b.pre |>.push (letBind dest (lit 0)) |>.push ite, expr := localName dest } private partial def checkedAdd (bits : Nat) (left right : Val) : M Val := do let a ← atom left let b ← atom right let dest ← fresh let sum := bin "add" a.expr b.expr - let ok := letBind dest sum + let ok := assignBind dest sum let cond := if bits == 256 then bin "lt" sum a.expr else bin "lt" (lit (2 ^ bits - 1)) sum - pure { pre := a.pre ++ b.pre |>.push (iteStmt cond overflowPanic ok), expr := localName dest } + pure { pre := a.pre ++ b.pre |>.push (letBind dest (lit 0)) |>.push (iteStmt cond overflowPanic ok), expr := localName dest } private partial def checkedMul (bits : Nat) (left right : Val) : M Val := do let a ← atom left let b ← atom right let dest ← fresh let prod := bin "mul" a.expr b.expr - let ok := letBind dest prod + let ok := assignBind dest prod -- Check the EVM-width product before the Solidity-width bound. For e.g. -- uint248, an overflowing 256-bit product can wrap below the uint248 bound. let bounded := if bits == 256 then ok else iteStmt (bin "lt" (lit (2 ^ bits - 1)) prod) overflowPanic ok let inner := iteStmt (bin "eq" (bin "div" prod a.expr) b.expr) bounded overflowPanic let ite := iteStmt (bin "eq" a.expr (lit 0)) bounded inner - pure { pre := a.pre ++ b.pre |>.push ite, expr := localName dest } + pure { pre := a.pre ++ b.pre |>.push (letBind dest (lit 0)) |>.push ite, expr := localName dest } private partial def cmp (op : String) (left right : Val) : M Val := do let a ← atom left @@ -625,10 +643,10 @@ private partial def lowerConditional (j : Json) : M Val := do let yes ← lowerExpr (← mField j "trueExpression") let no ← lowerExpr (← mField j "falseExpression") let dest ← fresh - let thenB := yes.pre.push (st s!"letVar {leanStr dest} {yes.expr}") - let elseB := no.pre.push (st s!"letVar {leanStr dest} {no.expr}") + let thenB := yes.pre.push (assignBind dest yes.expr) + let elseB := no.pre.push (assignBind dest no.expr) let ite := st s!"ite {cond.expr} {leanList thenB} {leanList elseB}" - pure { pre := cond.pre.push ite, expr := ex s!"localVar {leanStr dest}" } + pure { pre := cond.pre.push (letBind dest (lit 0)) |>.push ite, expr := ex s!"localVar {leanStr dest}" } private partial def lowerCast (j : Json) : M Val := do let targetExpr ← mField j "expression" @@ -688,6 +706,12 @@ private partial def lowerCall (j : Json) : M Val := do private partial def inlineFn (fnId : Nat) (args : Array Val) (at_ : Json) : M Val := do if (← get).stack.contains fnId then failAt at_ s!"recursive call {fnId}" let some fn := (← get).funs.find? fnId | failAt at_ s!"unresolved function {fnId}" + let saved := ← get + let savedYul := saved.yulNames + let savedFile := (← get).currentFile + let frameFile := (← get).nodeFile.find? fnId |>.getD (← get).currentFile + let frame := fnId :: (← get).stack + modify fun e => { e with stack := frame, currentFile := frameFile } if (field? fn "virtual").bind (fun v => v.getBool?.toOption) == some true then failAt fn "virtual dispatch is outside this slice" let mods ← mArr (← mField fn "modifiers") @@ -698,12 +722,6 @@ private partial def inlineFn (fnId : Nat) (args : Array Val) (at_ : Json) : M Va let rets ← mArr (← mField (← mField fn "returnParameters") "parameters") unless rets.size == 1 do failAt fn "only single-value helpers are inlined" let retName ← mStr (← mField rets[0]! "name") - let saved := ← get - let savedYul := saved.yulNames - let savedFile := (← get).currentFile - let frameFile := (← get).nodeFile.find? fnId |>.getD (← get).currentFile - let frame := fnId :: (← get).stack - modify fun e => { e with stack := frame, currentFile := frameFile } let mut pre : Array String := #[] let mut yul := savedYul for i in [:params.size] do @@ -850,6 +868,8 @@ private def bindRoot (fn : Json) : M (Array SrcParam) := do pure out private def lowerRoot (fn : Json) : M (Array String × Array SrcParam) := do + let rootId ← mNat (← mField fn "id") + modify fun e => { e with stack := [rootId] } let srcParams ← bindRoot fn if (field? fn "virtual").bind (fun v => v.getBool?.toOption) == some true then failAt fn "virtual dispatch is outside this slice" @@ -889,6 +909,8 @@ private def lowerRoot (fn : Json) : M (Array String × Array SrcParam) := do private partial def index (file : String) (contract? : Option String) (j : Json) : M Unit := do match j with | .obj o => + if let some name := optStr j "name" then + modify fun e => { e with sourceNames := name :: e.sourceNames } if (field? j "nodeType").isSome then if let some idj := field? j "id" then let id ← mNat idj @@ -1117,17 +1139,17 @@ private def importSlice for (member, ty) in mem.members do if let some proj := env.projections.find? (fun q => q.parameter == p.name && q.member == member) then let some tySyn := paramTypeSyntax ty - | throwError "unsupported projected type {ty}" + | (failAt fn s!"unsupported projected type {ty}").run' env if modelParamNames.contains proj.modelParam then - throwError "projected parameter name collision: {proj.modelParam}" + (failAt fn s!"projected parameter name collision: {proj.modelParam}").run' env modelParamNames := modelParamNames.push proj.modelParam modelParams := modelParams.push s!"({ "{ " }name := {leanStr proj.modelParam}, ty := {tySyn}{ " }" } : Compiler.CompilationModel.Param)" seen := true - unless seen do throwError "struct parameter {p.name} was not read" + unless seen do (failAt fn s!"struct parameter {p.name} was not read").run' env else let some tySyn := env.scalarTy.find? p.id | throwError "missing parameter type" - if modelParamNames.contains p.name then throwError "projected parameter name collision: {p.name}" + if modelParamNames.contains p.name then (failAt fn s!"projected parameter name collision: {p.name}").run' env modelParamNames := modelParamNames.push p.name modelParams := modelParams.push s!"({ "{ " }name := {leanStr p.name}, ty := {tySyn}{ " }" } : Compiler.CompilationModel.Param)" @@ -1136,7 +1158,7 @@ private def importSlice let mut out : Array String := #[] for r in rets do let ty ← mType r - let some tySyn := paramTypeSyntax ty | throwError "unsupported return type {ty}" + let some tySyn := paramTypeSyntax ty | failAt r s!"unsupported return type {ty}" out := out.push tySyn pure out) |>.run' env let mutability ← str (← field fn "stateMutability") diff --git a/Compiler/SoliditySlice/README.md b/Compiler/SoliditySlice/README.md index 9e59646ef4..0010f8afb4 100644 --- a/Compiler/SoliditySlice/README.md +++ b/Compiler/SoliditySlice/README.md @@ -53,8 +53,8 @@ are checked for parameter-name collisions. ## Determinism and trust Function selection uses the contract, name, and full parameter-type list. Helper -resolution uses solc declaration IDs. Generated names cannot capture Solidity -identifiers; helper-local environments are restored after inlining. Field and +resolution uses solc declaration IDs. Generated names are valid compiler identifiers and checked against source names +and potential parameter projections; helper-local environments are restored after inlining. Field and function ordering is deterministic. The digest uses framed JSON containing the complete solc input, selected signature, release identity, and importer sources. The report records the checksum actually verified for the invoked compiler. @@ -83,3 +83,6 @@ unrelated unsupported storage, explicit rejection of named arguments and implicit returns, reached/unreached unsupported loops, and detection of changes to arithmetic, field reads, layout, and return order. The smoke interpreter checks do not replace kernel proofs. + +See [TESTING.md](TESTING.md) for reusable A/B/C execution, seeded fuzzing, +generated programs, reduction, mutations and the exact observation boundary. diff --git a/Compiler/SoliditySlice/TESTING.md b/Compiler/SoliditySlice/TESTING.md new file mode 100644 index 0000000000..972641bd91 --- /dev/null +++ b/Compiler/SoliditySlice/TESTING.md @@ -0,0 +1,115 @@ +# Differential validation of Solidity slices + +The reusable engine lives in Verity. A consumer supplies a JSON fixture naming +its source/signature, scalar input domains, argument projections, storage recipes +and fixed regression corpus. No consumer arithmetic is reimplemented in the engine. + +## Execution contract + +Every case takes three paths: + +- **A:** original Solidity, compiled by checksum-pinned solc, deployed and called in Foundry. +- **B:** the imported `CompilationModel`, executed by the same `Denote.execStmtList` + used in the proofs, with Verity's pure Keccak engine for storage slots. +- **C:** that model compiled by the normal Verity compiler, compiled from Yul by + the pinned solc, deployed and called in the same Foundry harness. + +Python constructs source calldata with `eth-abi` and storage slots with +`eth-hash`; it does not share Verity's encoders or arithmetic. Ignored ABI members, +including dynamic arrays, receive deterministic noise. Unused packed bits, +adjacent storage words, keys and unrelated slots are varied too. Each EVM case +uses a restored snapshot. V1 accepts read-only slices, rejects any recorded +storage write, and compares the initialized/observed storage words on all paths. +This is not a test of arbitrary state-writing contracts. + +The comparator checks success/revert, exact scalar return words and observed +storage on A/B/C, plus exact return/revert bytes on A/C. B does not expose panic +payloads. Model calldata is the explicit scalar projection, not a claim that +Verity implements the complete source ABI. Gas and internal memory allocation +identity are not equivalence criteria. Resource exhaustion, invalid input, +unsupported compilation and tool failures are **harness errors**, never reverts. + +The root must have a public/external ABI, static scalar returns, no constructor +arguments and the Osaka profile. Missing support is rejected before comparison. +The driver refuses uncovered models and does not interpret unknown constructors +through Denote's fallback. `keccakMemorySlice` is unreachable in this whitelist. + +## Commands + +Run from a Lake workspace (Verity or a downstream project): + +```sh +scripts/check_solidity_differential.sh \ + --config Contracts/SoliditySliceSmoke/differential.json \ + --output .lake/differential/smoke --seed 2438 --cases 128 + +scripts/check_solidity_differential.sh \ + --programs 5 --cases 16 --seed 2438 --output .lake/differential/programs + +scripts/check_solidity_differential.sh \ + --mutations --output .lake/differential/mutants +``` + +Downstream projects invoke the same script from their pinned Verity dependency. +The script installs its Python dependencies in the caller's `.lake` directory. +The ordinary proof build has no Python package dependency. + +Each generated expression is run directly and through hygienic renamed/helper +variants, including a scalar projection whose name collides with the temporary +prefix. The grammar exercises uint8/16/128/248/256 casts, checked arithmetic, +ternaries and helper composition. Independent source executions must agree +between metamorphic variants as well as between A/B/C. + +## Failure evidence and reduction + +A campaign records the config, seed/values, materialized inputs, source closure, +solc inputs/outputs, generated Yul, both creation bytecodes, tool versions, +implementation/source hashes, import digest, logs and per-route results. +Fresh imports are checked against the recorded digest; changed implementation +or binary artifacts invalidate replay. The original workspace/toolchain is +required for replay; archived sources support auditing and reconstruction. + +```sh +scripts/check_solidity_differential.sh --replay --output .lake/differential/smoke +scripts/check_solidity_differential.sh --reduce --reduce-seconds 120 \ + --output .lake/differential/failing-campaign +``` + +The reducer preserves the mismatch category, not merely any failure. It reduces +inputs, and additionally reduces the typed expression tree for generated programs. +It does not delete text from arbitrary production Solidity. Original evidence is +retained; reduced cases go in a child directory. Reduction is budgeted and does +not claim a globally minimal counterexample. + +Mutation campaigns copy sources and create private copy-on-write build caches; +only dependency packages are shared. They compile actual importer and Denote +mutants. A changed comparison, wrong field slot, wrong packed mask and wrong +storage read must produce runtime divergences. A compile error/timeout is an +**invalid mutant**, not a detected semantic bug. Mutation outputs must be fresh. + +## Extension and rollout policy + +1. Keep the fixed source/case corpus and existing five Midnight proofs green. +2. Add positive, boundary, interaction and explicit-rejection cases for each new + supported construct. Add a runtime-killed mutant where the risk warrants it. +3. Run fixed seeds on PRs; longer seeded input/program/mutation campaigns on a + schedule and manually. Record every seed and publish failure artifacts. +4. Turn minimized counterexamples into reviewed, permanent regressions. +5. Keep importability, Denote coverage, compilability, compiler-proof coverage + and tested observables separate. No test count establishes source equivalence. + +The implementation sequence is shared protocol/A-B-C runner; input fuzzing and +consumer integration; generated programs/reducers; mutation checks and CI gates. +The initial PR/nightly budgets are 10/60 minutes excluding toolchain preparation; +CI timeouts are failures, not truncated passing campaigns. + +## EVMYulLean assessment + +The pinned dependency has a bytecode executor, `EvmYul.EVM.Ξ`, with explicit +execution exceptions including fuel exhaustion. The in-repo native harness is +primarily a Yul semantic/proof harness, not a drop-in runner for these deployed +bytecodes. A fourth path needs a concrete account/block/calldata setup, fork and +opcode conformance checks, independent return/revert/exception decoding, and +cross-checks against Foundry. It is not claimed as executed by this harness. +A/B/C is the implemented gate; adding D must not replace independent Foundry +execution or collapse an unsupported opcode into a contract revert. diff --git a/Contracts/SoliditySliceSmoke/Smoke.lean b/Contracts/SoliditySliceSmoke/Smoke.lean index 05e06db955..04ca6e58c3 100644 --- a/Contracts/SoliditySliceSmoke/Smoke.lean +++ b/Contracts/SoliditySliceSmoke/Smoke.lean @@ -63,7 +63,7 @@ theorem smoke_step_credit : execStmt sliceOracle slice.model.fields (witnessState 100 10 0 0 0 50 100) (peel smokeBody).1 = .continue { witnessState 100 10 0 0 0 50 100 with - bindings := bindValue (witnessBindings 100) "slice.tmp.0" 100 } := by + bindings := bindValue (witnessBindings 100) "_verity_slice_tmp_0" 100 } := by dsimp only [peel, smokeBody, slice.model] have hmi : ("m_maturity" == "id") = false := by decide have hmu : ("m_maturity" == "user") = false := by decide diff --git a/Contracts/SoliditySliceSmoke/differential-cases.json b/Contracts/SoliditySliceSmoke/differential-cases.json new file mode 100644 index 0000000000..294a23b15e --- /dev/null +++ b/Contracts/SoliditySliceSmoke/differential-cases.json @@ -0,0 +1,4 @@ +[ + {"name":"success", "credit":"100", "pending":"10", "lastLoss":"0", "lossFactor":"0", "lastAccrual":"0", "timestamp":"50", "maturity":"100", "id":"1", "user":"2"}, + {"name":"revert", "credit":"1", "pending":"2", "lastLoss":"0", "lossFactor":"0", "lastAccrual":"0", "timestamp":"1", "maturity":"1", "id":"1", "user":"2"} +] diff --git a/Contracts/SoliditySliceSmoke/differential.json b/Contracts/SoliditySliceSmoke/differential.json new file mode 100644 index 0000000000..64f83e3d42 --- /dev/null +++ b/Contracts/SoliditySliceSmoke/differential.json @@ -0,0 +1,63 @@ +{ + "project": ".", + "entry": "Slice.sol", + "contract": "C", + "function": "f", + "param_types": [ + "struct Mkt", + "bytes32", + "address" + ], + "variables": { + "maturity": 256, + "id": 256, + "user": 160, + "credit": 128, + "pending": 128, + "lastLoss": 128, + "lastAccrual": 128, + "lossFactor": 128, + "timestamp": 256 + }, + "arguments": { + "m.maturity": "maturity", + "id": "id", + "user": "user" + }, + "timestamp": "timestamp", + "ordered_pairs": [ + [ + "lastLoss", + "lossFactor" + ], + [ + "lastAccrual", + "timestamp" + ] + ], + "storage": [ + { + "field": "position", + "keys": [ + "id", + "user" + ], + "members": { + "credit": "credit", + "pendingFee": "pending", + "lastLossFactor": "lastLoss", + "lastAccrual": "lastAccrual" + } + }, + { + "field": "marketState", + "keys": [ + "id" + ], + "members": { + "lossFactor": "lossFactor" + } + } + ], + "corpus": "differential-cases.json" +} diff --git a/scripts/check_solidity_differential.sh b/scripts/check_solidity_differential.sh new file mode 100755 index 0000000000..62b6a03bc5 --- /dev/null +++ b/scripts/check_solidity_differential.sh @@ -0,0 +1,11 @@ +#!/bin/sh +set -eu +# Keep the caller's Lake workspace: this script also runs from downstream repos. +verity_test_root=$(CDPATH= cd -- "$(dirname -- "$0")/.." && pwd) +verity_test_venv=".lake/slice-testing-venv" +if [ ! -x "$verity_test_venv/bin/python" ]; then + python3 -m venv "$verity_test_venv" +fi +"$verity_test_venv/bin/python" -m pip install --disable-pip-version-check -q -r "$verity_test_root/scripts/solidity_differential/requirements.txt" +python3 "$verity_test_root/scripts/setup_solc_slice.py" --output .lake/solidity-import/solc-0.8.34 +exec "$verity_test_venv/bin/python" "$verity_test_root/scripts/solidity_slice_differential.py" "$@" diff --git a/scripts/solidity_differential/Runner.t.sol b/scripts/solidity_differential/Runner.t.sol new file mode 100644 index 0000000000..e1a2e18877 --- /dev/null +++ b/scripts/solidity_differential/Runner.t.sol @@ -0,0 +1,80 @@ +// SPDX-License-Identifier: MIT +pragma solidity 0.8.34; + +interface VmSlice { + function readFile(string calldata) external view returns (string memory); + function readFileBinary(string calldata) external view returns (bytes memory); + function writeFile(string calldata, string calldata) external; + function writeLine(string calldata, string calldata) external; + function parseJsonString(string calldata, string calldata) external pure returns (string memory); + function parseJsonUint(string calldata, string calldata) external pure returns (uint256); + function parseJsonBytes(string calldata, string calldata) external pure returns (bytes memory); + function parseUint(string calldata) external pure returns (uint256); + function toString(uint256) external pure returns (string memory); + function toString(bytes calldata) external pure returns (string memory); + function store(address, bytes32, bytes32) external; + function load(address, bytes32) external view returns (bytes32); + function warp(uint256) external; + function snapshotState() external returns (uint256); + function revertToStateAndDelete(uint256) external returns (bool); + function record() external; + function accesses(address) external returns (bytes32[] memory, bytes32[] memory); +} + +/// Executes the actual creation/runtime bytecode. No contract arithmetic or +/// storage layout logic is duplicated here. All slices in v1 are read-only. +contract SliceDifferentialTest { + VmSlice constant vm = VmSlice(address(uint160(uint256(keccak256("hevm cheat code"))))); + uint256 constant CALL_GAS = 30_000_000; + + function deploy(string memory path) internal returns (address target) { + bytes memory code = vm.readFileBinary(path); + assembly { target := create(0, add(code, 32), mload(code)) } + require(target != address(0) && target.code.length != 0, "harness: deployment failed"); + } + + function number(string memory json, string memory key) internal pure returns (uint256) { + return vm.parseUint(vm.parseJsonString(json, key)); + } + + function runRoute(string memory route, address target, uint256 count) internal { + string memory output = string.concat("out/", route, ".txt"); + vm.writeFile(output, ""); + for (uint256 i; i < count; ++i) { + uint256 snapshot = vm.snapshotState(); + string memory json = vm.readFile(string.concat("evm-cases/", vm.toString(i), ".json")); + string memory prefix = ""; + uint256 entries = vm.parseJsonUint(json, string.concat(prefix, ".storageCount")); + for (uint256 k; k < entries; ++k) { + string memory row = string.concat(prefix, ".storage[", vm.toString(k), "]"); + vm.store(target, bytes32(number(json, string.concat(row, "[0]"))), + bytes32(number(json, string.concat(row, "[1]")))); + } + vm.warp(number(json, string.concat(prefix, ".timestamp"))); + bytes memory data = vm.parseJsonBytes(json, string.concat(prefix, ".", route, "Calldata")); + vm.record(); + uint256 beforeGas = gasleft(); + (bool ok, bytes memory returned) = target.call{gas: CALL_GAS}(data); + uint256 spent = beforeGas - gasleft(); + // Exhausted gas or exceptional halts are resource/harness failures, + // never evidence that the Solidity and model both reverted. + require(spent < CALL_GAS, "harness: execution resource limit"); + (, bytes32[] memory writes) = vm.accesses(target); + require(writes.length == 0, "harness: read-only slice wrote storage"); + string memory slots; + uint256 observed = vm.parseJsonUint(json, string.concat(prefix, ".observeCount")); + for (uint256 k; k < observed; ++k) { + uint256 slot = number(json, string.concat(prefix, ".observe[", vm.toString(k), "]")); + slots = string.concat(slots, " ", vm.toString(uint256(vm.load(target, bytes32(slot))))); + } + vm.writeLine(output, string.concat(vm.toString(i), ok ? " ok " : " revert ", vm.toString(returned), slots)); + require(vm.revertToStateAndDelete(snapshot), "harness: snapshot restore failed"); + } + } + + function test_differential() public { + uint256 count = vm.parseJsonUint(vm.readFile("cases.json"), ".count"); + runRoute("source", deploy("source.bin"), count); + runRoute("compiled", deploy("compiled.bin"), count); + } +} diff --git a/scripts/solidity_differential/__init__.py b/scripts/solidity_differential/__init__.py new file mode 100644 index 0000000000..c2637640b8 --- /dev/null +++ b/scripts/solidity_differential/__init__.py @@ -0,0 +1 @@ +"""Differential execution of covered Solidity imports (test tooling only).""" diff --git a/scripts/solidity_differential/cases.py b/scripts/solidity_differential/cases.py new file mode 100644 index 0000000000..ea3e35d426 --- /dev/null +++ b/scripts/solidity_differential/cases.py @@ -0,0 +1,117 @@ +"""Independent ABI/storage fixture construction from solc metadata.""" +import random + +MODULUS = 1 << 256 + + +def keccak(data): + from eth_hash.auto import keccak as hash_bytes + return hash_bytes(data) + + + +def canonical(abi): + return "(" + ",".join(map(canonical, abi["components"])) + ")" + abi["type"][5:] if abi["type"].startswith("tuple") else abi["type"] + + +def abi_value(abi, path, arguments, noise=b""): + ty = abi["type"] + salt = keccak(noise + path.encode()) if noise else bytes(32) + if ty.endswith("]"): + base, length = ty.rsplit("[", 1) + length = length[:-1] + return [abi_value({**abi, "type": base}, path + f"[{i}]", arguments, noise) for i in range(int(length) if length else salt[0] % 3)] + if ty == "tuple": + return tuple(abi_value(child, path + "." + child["name"], arguments, noise) for child in abi["components"]) + bits = 160 if ty == "address" else (1 if ty == "bool" else int(ty[4:] or 256) if ty.startswith("uint") else int(ty[3:] or 256) if ty.startswith("int") else int(ty[5:]) * 8 if ty.startswith("bytes") and ty != "bytes" else 256) + n = arguments.get(path, int.from_bytes(salt, "big") % (1 << bits)) + if ty.startswith("int") and path not in arguments and n >= 1 << (bits - 1): + n -= 1 << bits + if ty == "address": + return n.to_bytes(20, "big") + if ty.startswith("bytes"): + return n.to_bytes(int(ty[5:]), "big") if ty != "bytes" else salt[:salt[0] % 3] + if ty == "string": + return salt[:salt[0] % 3].hex() + if ty == "bool": + if n not in (0, 1): + raise ValueError("invalid boolean fixture") + return bool(n) + return n + + +def mapping_slot(base, key): + return int.from_bytes(keccak(key.to_bytes(32, "big") + base.to_bytes(32, "big")), "big") + + +def storage_words(layout, recipes, values): + result = {} + for recipe in recipes: + field = next(f for f in layout["storage"] if f["label"] == recipe["field"]) + slot, type_id = int(field["slot"]), field["type"] + for name in recipe["keys"]: + ty = layout["types"][type_id] + if ty["encoding"] != "mapping": + raise ValueError("storage recipe key count exceeds mapping depth") + slot = mapping_slot(slot, values[name]) + type_id = ty["value"] + members = layout["types"][type_id]["members"] + for member, name in recipe["members"].items(): + item = next(m for m in members if m["label"] == member) + ty = layout["types"][item["type"]] + if not ty["label"].startswith("uint"): + raise ValueError("only unsigned packed storage fixtures are supported") + width = int(ty["numberOfBytes"]) * 8 + value = values[name] + if not 0 <= value < 1 << width: + raise ValueError(f"out of range storage value {member}") + address = (slot + int(item["slot"])) % MODULUS + shift = int(item["offset"]) * 8 + mask = ((1 << width) - 1) << shift + # Fill unused packed members with noise, not assumed zeros. + old = result.get(address, int.from_bytes(keccak(address.to_bytes(32, "big")), "big")) + result[address] = (old & ~mask) | (value << shift) + return result + + +def materialize(config, metadata, abi, layout, values, name): + from eth_abi import encode + arguments = {path: values[var] for path, var in config["arguments"].items()} + model_args = [] + projected = {p["modelParam"]: p["parameter"] + "." + p["member"] for p in metadata["projections"]} + for param in metadata["params"]: + model_args.append(arguments[projected.get(param, param)]) + signature = abi["name"] + "(" + ",".join(map(canonical, abi["inputs"])) + ")" + source_data = keccak(signature.encode())[:4] + encode( + [canonical(x) for x in abi["inputs"]], [abi_value(x, x["name"], arguments, name.encode()) for x in abi["inputs"]]) + compiled_data = bytes.fromhex("12345678") + b"".join(n.to_bytes(32, "big") for n in model_args) + storage = storage_words(layout, config.get("storage", []), values) + # Adjacent slots and unrelated words detect offsets / accidental reads. + for slot in list(storage): + for neighbor in ((slot - 1) % MODULUS, (slot + 1) % MODULUS): + storage.setdefault(neighbor, int.from_bytes(keccak(neighbor.to_bytes(32, "big")), "big")) + for slot in range(4): + storage.setdefault(slot, MODULUS - slot - 1) + observed = sorted(storage) + return {"id": name, "args": list(map(str, model_args)), "timestamp": str(values.get(config.get("timestamp", ""), 0)), + "storage": [[str(k), str(v)] for k, v in sorted(storage.items())], "observe": list(map(str, observed)), + "storageCount": len(storage), "observeCount": len(observed), + "sourceCalldata": "0x" + source_data.hex(), "compiledCalldata": "0x" + compiled_data.hex()} + + +def generate(config, count, seed, corpus): + rng = random.Random(seed) + domains = config["variables"] + seeds = [{key: int(row.get(key, config.get("defaults", {}).get(key, 0))) for key in domains} for row in corpus] or [{key: 0 for key in domains}] + result = [(row.get("name", f"corpus-{i}"), values) for i, (row, values) in enumerate(zip(corpus, seeds))] + for i in range(count): + values = dict(seeds[i % len(seeds)]) if i % 3 else {} + for key, bits in domains.items(): + edge = [0, 1, 2, (1 << bits) - 1, (1 << bits) - 2, 1 << (bits - 1)] + if key not in values or rng.randrange(3) == 0: + values[key] = rng.choice(edge) if rng.randrange(2) else rng.getrandbits(bits) + if i % 2 == 0: + for low, high in config.get("ordered_pairs", []): + values[low], values[high] = sorted((values[low], values[high])) + result.append((f"seed-{seed}-{i}", values)) + return result diff --git a/scripts/solidity_differential/engine.py b/scripts/solidity_differential/engine.py new file mode 100644 index 0000000000..221be8eabb --- /dev/null +++ b/scripts/solidity_differential/engine.py @@ -0,0 +1,232 @@ +"""Batch A/B/C execution with explicit infrastructure failures and replay artifacts.""" +import hashlib +import json +from pathlib import Path +import shutil +import subprocess +import time + +from .cases import canonical, generate, materialize + +ROOT = Path(__file__).resolve().parents[2] +WORKSPACE = Path.cwd().resolve() +SOLC = WORKSPACE / ".lake/solidity-import/solc-0.8.34" + + +class HarnessError(RuntimeError): + pass + + +def write_json(path, obj): + path.write_text(json.dumps(obj, indent=2) + "\n") + + +def command(argv, *, cwd=WORKSPACE, timeout=180, stdin=None, log=None): + try: + result = subprocess.run(list(map(str, argv)), cwd=cwd, input=stdin, text=True, capture_output=True, timeout=timeout) + except subprocess.TimeoutExpired as error: + raise HarnessError(f"resource_limit: {argv[0]} exceeded {timeout}s") from error + if log: + Path(log).write_text(result.stdout + result.stderr) + if result.returncode: + raise HarnessError(f"tool_failure ({result.returncode}): {' '.join(map(str, argv))}\n{(result.stdout + result.stderr)[-4000:]}") + return result.stdout + + +def solc_compile(request, project, out): + write_json(out.with_suffix(".input.json"), request) + stdout = command([SOLC, "--base-path", project, "--standard-json"], stdin=json.dumps(request), timeout=180) + result = json.loads(stdout) + write_json(out.with_suffix(".output.json"), result) + errors = [e.get("formattedMessage", e["message"]) for e in result.get("errors", []) if e["severity"] == "error"] + if errors: + raise HarnessError("solc_failure: " + "\n".join(errors)) + return result + + +def implementation_hashes(): + hashes = {str(p.relative_to(ROOT)): hashlib.sha256(p.read_bytes()).hexdigest() + for directory in ("Compiler", "Verity", "scripts/solidity_differential") + for p in (ROOT / directory).rglob("*") if p.suffix in (".lean", ".py", ".sol", ".txt")} + + for name in ("scripts/solidity_slice_differential.py", "lake-manifest.json", "lean-toolchain"): + hashes[name] = hashlib.sha256((ROOT / name).read_bytes()).hexdigest() + return hashes + + +def prepare(config_path, output): + config_path = Path(config_path).resolve() + config = json.loads(config_path.read_text()) + project = (config_path.parent / config["project"]).resolve() + output = Path(output).resolve() + output.mkdir(parents=True, exist_ok=True) + (output / "out").mkdir(exist_ok=True) + if not isinstance(config.get("variables"), dict) or not config["variables"]: + raise HarnessError("fixture_unsupported: nonempty variable domains required") + for name, bits in config["variables"].items(): + if not isinstance(bits, int) or not 1 <= bits <= 256: + raise HarnessError("invalid variable width: " + name) + settings = config.get("settings", {}) + if set(settings) - {"evmVersion", "viaIR", "optimizer", "metadata"}: + raise HarnessError("fixture_unsupported: unknown compiler settings") + profile = {"evmVersion": "osaka", "viaIR": True, "optimizer": {"enabled": True, "runs": 466}, + "metadata": {"bytecodeHash": "none"}, **settings} + if profile["evmVersion"] != "osaka": + raise HarnessError("fixture_unsupported: this runner currently targets Osaka") + command(["lake", "build", "Compiler.SoliditySlice.Import", "Compiler.SoliditySlice.Differential"], + timeout=1800, log=output / "build.log") + # Keep the importer and reference compiler profile identical. + driver = output / "Driver.lean" + driver.write_text('import Compiler.SoliditySlice.Import\nimport Compiler.SoliditySlice.Differential\n' + + 'solidity_slice_import tested\n' + + f' slice_root {json.dumps(str(project))} slice_entry {json.dumps(config["entry"])}\n' + + f' slice_contract {json.dumps(config["contract"])} slice_function {json.dumps(config["function"])}\n' + + ' slice_param_tys [' + ', '.join(map(json.dumps, config["param_types"])) + ']\n' + + f' slice_solc "0.8.34+commit.80d5c536" slice_via_ir {str(profile["viaIR"]).lower()} slice_evm {json.dumps(profile["evmVersion"])}\n' + + f' slice_optimizer {str(profile["optimizer"]["enabled"]).lower()} slice_runs {profile["optimizer"]["runs"]} slice_bytecode_hash {json.dumps(profile["metadata"]["bytecodeHash"])}\n' + + 'def main (args : List String) : IO UInt32 :=\n' + + ' Compiler.CompilationModel.SoliditySlice.Differential.run tested.model tested.report args\n') + command(["lake", "env", "lean", "--run", driver, "describe", output / "model.json", output / "model.yul"], + timeout=300, log=output / "import.log") + metadata = json.loads((output / "model.json").read_text()) + if not metadata["compilable"]: + raise HarnessError("compilation_unsupported: " + metadata["compileError"]) + imported_settings = json.loads(metadata["settings"]) + source_request = {"language": "Solidity", "sources": {config["entry"]: {"urls": [config["entry"]]}}, + "settings": {**imported_settings, "outputSelection": {"*": {"*": ["abi", "storageLayout", "evm.bytecode.object", "evm.deployedBytecode.object"]}}}} + source_result = solc_compile(source_request, project, output / "source") + contract = source_result["contracts"][config["entry"]][config["contract"]] + constructors = [x for x in contract["abi"] if x["type"] == "constructor"] + if constructors and constructors[0]["inputs"]: + raise HarnessError("fixture_unsupported: constructor arguments require an explicit deployment fixture") + matches = [x for x in contract["abi"] if x["type"] == "function" and x["name"] == config["function"] + and [p.get("internalType", p["type"]).replace(" payable", "") for p in x["inputs"]] == config["param_types"]] + if len(matches) != 1: + raise HarnessError("fixture_unsupported: selected function ABI is missing or ambiguous") + abi = matches[0] + if any(p["type"] in ("bytes", "string") or "[" in p["type"] or p["type"] == "tuple" for p in abi["outputs"]): + raise HarnessError("fixture_unsupported: only static scalar returns are compared") + source_bin = bytes.fromhex(contract["evm"]["bytecode"]["object"]) + (output / "source.bin").write_bytes(source_bin) + yul_request = {"language": "Yul", "sources": {"Model.yul": {"content": (output / "model.yul").read_text()}}, + "settings": {"evmVersion": profile["evmVersion"], "optimizer": profile["optimizer"], + "outputSelection": {"*": {"*": ["evm.bytecode.object", "evm.deployedBytecode.object"]}}}} + compiled_result = solc_compile(yul_request, project, output / "compiled") + compiled_contracts = list(compiled_result["contracts"]["Model.yul"].values()) + if len(compiled_contracts) != 1: + raise HarnessError("harness: expected one compiled Yul object") + (output / "compiled.bin").write_bytes(bytes.fromhex(compiled_contracts[0]["evm"]["bytecode"]["object"])) + shutil.copyfile(Path(__file__).with_name("Runner.t.sol"), output / "Runner.t.sol") + (output / "foundry.toml").write_text('[profile.default]\nsrc = "empty"\ntest = "."\n' + + f'solc = {json.dumps(str(SOLC))}\nevm_version = "osaka"\noptimizer = true\nvia_ir = true\n' + + 'gas_limit = 9223372036854775807\ncode_size_limit = 100000\n' + + 'fs_permissions = [{ access = "read-write", path = "./" }]\n') + manifest = {"config": config, "configPath": str(config_path), "model": metadata, + "verity": command(["git", "rev-parse", "HEAD"], cwd=ROOT).strip(), + "solcSha256": hashlib.sha256(SOLC.read_bytes()).hexdigest(), + "forge": command(["forge", "--version"]).strip(), + "observables": ["success/revert", "return words", "observed storage", "no storage writes", "A/C revert payload"], + "exclusions": ["Denote revert payload", "gas equivalence", "memory allocation identity", "full ABI equivalence"]} + # Archive the source closure loaded by solc. Reject paths escaping the project. + for logical in source_result["sources"]: + path = (project / logical).resolve() + if not path.is_relative_to(project): + raise HarnessError("source outside project: " + logical) + target = output / "sources" / logical + target.parent.mkdir(parents=True, exist_ok=True) + shutil.copyfile(path, target) + manifest["sourceHashes"] = {str(p.relative_to(output / "sources")): hashlib.sha256(p.read_bytes()).hexdigest() + for p in (output / "sources").rglob("*.sol")} + write_json(output / "abi.json", abi) + write_json(output / "layout.json", contract["storageLayout"]) + manifest["implementationHashes"] = implementation_hashes() + manifest["artifactHashes"] = {name: hashlib.sha256((output / name).read_bytes()).hexdigest() + for name in ("source.bin", "compiled.bin", "model.yul", "Driver.lean", "Runner.t.sol", "foundry.toml", "abi.json", "layout.json") + if (output / name).exists()} + write_json(output / "manifest.json", manifest) + return config, metadata, abi, contract["storageLayout"], driver + + +def parse_evm(path, cases): + lines = path.read_text().splitlines() + if len(lines) != len(cases): + raise HarnessError("missing EVM results") + results = [] + for i, (line, case) in enumerate(zip(lines, cases)): + index, status, data, *storage = line.split() + if index != str(i) or status not in ("ok", "revert") or len(storage) != len(case["observe"]): + raise HarnessError("invalid EVM result row") + raw = bytes.fromhex(data.removeprefix("0x")) + if status == "ok" and len(raw) % 32: + raise HarnessError("invalid scalar ABI result length") + results.append({"id": case["id"], "status": status, "data": data, + "words": [str(int.from_bytes(raw[k:k+32], "big")) for k in range(0, len(raw), 32)] if status == "ok" else [], + "storage": storage}) + return results + + +def execute(output, cases): + output = Path(output).resolve() + manifest = json.loads((output / "manifest.json").read_text()) + if implementation_hashes() != manifest["implementationHashes"]: + raise HarnessError("implementation changed; prepare a new campaign") + if not cases: + raise HarnessError("empty campaign is not a passing comparison") + for name, digest in manifest.get("artifactHashes", {}).items(): + if hashlib.sha256((output / name).read_bytes()).hexdigest() != digest: + raise HarnessError("campaign artifact changed: " + name) + if hashlib.sha256(SOLC.read_bytes()).hexdigest() != manifest["solcSha256"]: + raise HarnessError("compiler binary changed") + if command(["forge", "--version"]).strip() != manifest["forge"]: + raise HarnessError("Foundry version changed") + if len({case["id"] for case in cases}) != len(cases): + raise HarnessError("duplicate case identities") + write_json(output / "model-cases.json", cases) + write_json(output / "cases.json", {"count": len(cases)}) + # Parse one small document per EVM case, avoiding quadratic JSON work. + (output / "evm-cases").mkdir(exist_ok=True) + for index, case in enumerate(cases): + write_json(output / "evm-cases" / f"{index}.json", case) + # Delete old outputs: a broken process cannot pass by leaving stale results. + for file in ("model-results.json", "out/source.txt", "out/compiled.txt"): + (output / file).unlink(missing_ok=True) + command(["lake", "env", "lean", "--run", output / "Driver.lean", "run", output / "model-cases.json", output / "model-results.json", manifest["model"]["digest"]], + timeout=300, log=output / "model-run.log") + command(["forge", "test", "--root", output, "--match-contract", "SliceDifferentialTest", "-vv"], + cwd=output, timeout=300, log=output / "forge.log") + source = parse_evm(output / "out/source.txt", cases) + compiled = parse_evm(output / "out/compiled.txt", cases) + model = json.loads((output / "model-results.json").read_text()) + if len(model) != len(cases): + raise HarnessError("missing model results") + divergences = [] + for case, a, b, c in zip(cases, source, model, compiled): + if b["status"] not in ("ok", "revert"): + raise HarnessError("invalid model status") + if b["id"] != case["id"]: + raise HarnessError("model result identity mismatch") + observable = lambda r: (r["status"], r["words"], r["storage"]) + if observable(a) != observable(b) or observable(b) != observable(c) or a["data"] != c["data"]: + divergences.append({"case": case, "source": a, "model": b, "compiled": c}) + report = {"cases": len(cases), "successes": sum(r["status"] == "ok" for r in source), + "reverts": sum(r["status"] == "revert" for r in source), "divergences": divergences} + write_json(output / "results.json", report) + return report + + +def campaign(config_path, output, count, seed): + started = time.monotonic() + config, metadata, abi, layout, _ = prepare(config_path, output) + corpus_path = config.get("corpus") + corpus = json.loads((Path(config_path).resolve().parent / corpus_path).read_text()) if corpus_path else [] + values = generate(config, count, seed, corpus) + write_json(Path(output) / "values.json", [{"name": name, **{k: str(v) for k, v in row.items()}} for name, row in values]) + cases = [materialize(config, metadata, abi, layout, row, name) for name, row in values] + manifest_path = Path(output) / "manifest.json" + manifest = json.loads(manifest_path.read_text()) + manifest["casesSha256"] = hashlib.sha256(json.dumps(cases, sort_keys=True).encode()).hexdigest() + write_json(manifest_path, manifest) + report = execute(output, cases) + report.update(seed=seed, seconds=round(time.monotonic() - started, 3)) + write_json(Path(output) / "results.json", report) + return report diff --git a/scripts/solidity_differential/mutations.py b/scripts/solidity_differential/mutations.py new file mode 100644 index 0000000000..27a5575880 --- /dev/null +++ b/scripts/solidity_differential/mutations.py @@ -0,0 +1,73 @@ +"""Compile actual importer/Denote mutants in isolated source/cache snapshots.""" +import json +from pathlib import Path +import shutil +import subprocess +import sys + +from .engine import ROOT, WORKSPACE, HarnessError, command, write_json + +MUTANTS = { + "import-comparison": ("Compiler/SoliditySlice/Import.lean", '| "<" => cmp "lt" left right', '| "<" => cmp "gt" left right'), + "import-field-slot": ("Compiler/SoliditySlice/Import.lean", 'slot := some {slot}', 'slot := some {slot + 1}'), + "denote-packed-mask": ("Verity/Core/Model/Denote.lean", '(2 ^ packed.width) - 1', '(2 ^ packed.width) - 2'), + "denote-storage-read": ("Verity/Core/Model/Denote.lean", ' world.readSlot (wordNormalize slot)\n', ' world.readSlot (wordNormalize (slot + 1))\n'), +} + + +def snapshot(destination): + destination.mkdir(parents=True, exist_ok=False) + paths = command(["git", "ls-files", "--cached", "--others", "--exclude-standard", "-z"], cwd=ROOT).split("\0") + for name in paths: + if not name or not (ROOT / name).is_file(): + continue + target = destination / name + target.parent.mkdir(parents=True, exist_ok=True) + shutil.copyfile(ROOT / name, target) + cache = destination / ".lake" + cache.mkdir(exist_ok=True) + (cache / "packages").symlink_to(WORKSPACE / ".lake/packages", target_is_directory=True) + # Private copy-on-write cache: no hardlinks/symlinks to mutable parent oleans. + if sys.platform == "darwin": + command(["cp", "-cR", WORKSPACE / ".lake/build", cache / "build"], timeout=300) + else: + command(["cp", "-a", "--reflink=auto", WORKSPACE / ".lake/build", cache / "build"], timeout=300) + (cache / "solidity-import").mkdir() + shutil.copy2(WORKSPACE / ".lake/solidity-import/solc-0.8.34", cache / "solidity-import/solc-0.8.34") + + +def mutation_campaign(output, selected=None): + output = Path(output).resolve() + output.mkdir(parents=True, exist_ok=True) + reports = [] + for name in selected or MUTANTS: + relative, before, after = MUTANTS[name] + directory = output / name + if directory.exists(): + raise HarnessError("mutation output already exists; choose a fresh output: " + str(directory)) + snapshot(directory) + source = directory / relative + text = source.read_text() + if text.count(before) != 1: + raise HarnessError("mutation anchor drifted: " + name) + source.write_text(text.replace(before, after)) + argv = [sys.executable, str(directory / "scripts/solidity_slice_differential.py"), + "--config", str(directory / "Contracts/SoliditySliceSmoke/differential.json"), + "--output", str(directory / ".lake/campaign"), "--cases", "8"] + try: + result = subprocess.run(argv, cwd=directory, text=True, capture_output=True, timeout=600) + (directory / "mutation.log").write_text(result.stdout + result.stderr) + report_file = directory / ".lake/campaign/results.json" + report = json.loads(report_file.read_text()) if report_file.exists() else {} + if result.returncode == 1 and report.get("divergences"): + status = "detected" + elif result.returncode == 0: + status = "survived" + else: + status = "invalid" # compilation/tool failure does not kill a mutant + except subprocess.TimeoutExpired: + status = "invalid" + reports.append({"mutant": name, "status": status, "path": relative, "output": str(directory)}) + write_json(output / "mutation-results.json", reports) + print(f"{name}: {status}", flush=True) + return {"mutants": reports, "divergences": [r for r in reports if r["status"] != "detected"]} diff --git a/scripts/solidity_differential/programs.py b/scripts/solidity_differential/programs.py new file mode 100644 index 0000000000..914998a1e6 --- /dev/null +++ b/scripts/solidity_differential/programs.py @@ -0,0 +1,111 @@ +"""Small typed source programs; solc/EVM supplies the expected behavior.""" +import copy +import json +from pathlib import Path +import random + +from .engine import campaign, write_json + + +def expression(rng, depth, bits): + if depth == 0 or rng.randrange(4) == 0: + return {"kind": rng.choice(["x", "y", "literal"]), "value": rng.choice([0, 1, 2, (1 << bits) - 1])} + return {"kind": "binary", "op": rng.choice(["+", "-", "*", "/"]), + "left": expression(rng, depth - 1, bits), "right": expression(rng, depth - 1, bits)} + + +def render_expr(node, bits, x="x", y="y"): + ty = f"uint{bits}" + if node["kind"] == "binary": + return f'({render_expr(node["left"], bits, x, y)} {node["op"]} {render_expr(node["right"], bits, x, y)})' + if node["kind"] == "literal": + return f'{ty}({node["value"]})' + return f'{ty}({x if node["kind"] == "x" else y})' + + +def source(spec): + bits = spec["bits"] + x = "_verity_slice_tmp_0" if spec["renamed"] else "x" + member = "tmp_0" if spec.get("projection_collision") else "maturity" + market = "_verity_slice" if spec.get("projection_collision") else "m" + expr = render_expr(spec["expression"], bits, x) + helper = "" + if spec["helper"]: + helper = f'''library L {{ + function work(uint{bits} x, uint{bits} y) internal pure returns (uint{bits}) {{ + return {render_expr(spec['expression'], bits)}; + }} +}}''' + expr = f"L.work(uint{bits}({x}), uint{bits}(y))" + return f'''// SPDX-License-Identifier: MIT +pragma solidity 0.8.34; +struct Mkt {{ uint256 ignored; uint128[] ignoredArray; uint256 {member}; }} +{helper} +contract C {{ + function f(Mkt memory {market}, uint256 {x}, uint256 y) external pure returns (uint256, uint256, uint256) {{ + uint256 stamp = {market}.{member}; + uint{bits} a = {expr}; + uint{bits} b = stamp < {x} ? a : uint{bits}(y); + return (uint256(a), uint256(b), stamp); + }} +}} +''' + + +def write_program(directory, spec): + directory = Path(directory) + directory.mkdir(parents=True, exist_ok=True) + (directory / "Slice.sol").write_text(source(spec)) + write_json(directory / "program.json", spec) + config = {"project": ".", "entry": "Slice.sol", "contract": "C", "function": "f", + "param_types": ["struct Mkt", "uint256", "uint256"], + "variables": {"x": 256, "y": 256, "maturity": 256}, + "arguments": {"_verity_slice.tmp_0" if spec.get("projection_collision") else "m.maturity": "maturity", "_verity_slice_tmp_0" if spec["renamed"] else "x": "x", "y": "y"}, + "storage": [], "corpus": "corpus.json"} + write_json(directory / "fixture.json", config) + write_json(directory / "corpus.json", [{"name": "zero", "x": "0", "y": "0", "maturity": "0"}, + {"name": "small", "x": "3", "y": "2", "maturity": "7"}, + {"name": "wrap-product", "x": str(1 << 128), "y": str(1 << 128), "maturity": "1"}]) + return directory / "fixture.json" + + +def smaller_expressions(node): + if node["kind"] == "binary": + yield node["left"] + yield node["right"] + for field in ("left", "right"): + for replacement in smaller_expressions(node[field]): + result = copy.deepcopy(node) + result[field] = replacement + yield result + if node != {"kind": "literal", "value": 0}: + yield {"kind": "literal", "value": 0} + + +def generated_campaign(output, count, cases, seed): + output = Path(output).resolve() + output.mkdir(parents=True, exist_ok=True) + rng = random.Random(seed) + reports = [] + for i in range(count): + bits = [8, 16, 128, 248, 256][i % 5] + tree = expression(rng, 2, bits) + # First five explicitly exercise each checked width, especially uint248. + if i < 5: + tree = {"kind": "binary", "op": "*", "left": {"kind": "x"}, "right": {"kind": "y"}} + reference = None + for variant in (0, 1, 2): + spec = {"bits": bits, "expression": tree, "renamed": variant == 1, "helper": variant != 0, "projection_collision": variant == 2} + directory = output / f"program-{i}-{variant}" + fixture = write_program(directory, spec) + report = campaign(fixture, directory / "run", cases, seed + i) + reports.append({"program": i, "variant": variant, **{k: v for k, v in report.items() if k != "divergences"}}) + write_json(output / "program-results.json", reports) + if report["divergences"]: + return {"programs": len(reports), "divergences": report["divergences"]} + # Renaming + helper extraction must preserve the observed behavior. + rows = (directory / "run/out/source.txt").read_text() + if reference is not None and reference != rows: + return {"programs": len(reports), "divergences": [{"metamorphic": str(directory)}]} + reference = rows + return {"programs": len(reports), "cases": sum(r["cases"] for r in reports), "divergences": []} diff --git a/scripts/solidity_differential/reduce.py b/scripts/solidity_differential/reduce.py new file mode 100644 index 0000000000..fdc035ec7e --- /dev/null +++ b/scripts/solidity_differential/reduce.py @@ -0,0 +1,80 @@ +"""Budgeted reducers which preserve the original observable mismatch category.""" +import copy +import hashlib +import json +from pathlib import Path +import shutil +import time + +from .cases import materialize +from .engine import campaign, execute, write_json +from .programs import smaller_expressions, write_program + + +def signature(divergence): + if "metamorphic" in divergence: + return ("metamorphic",) + routes = [divergence[k] for k in ("source", "model", "compiled")] + return tuple((key, routes[0][key] == routes[1][key], routes[1][key] == routes[2][key]) + for key in ("status", "words", "storage")) + (("revert-data", routes[0]["data"] == routes[2]["data"]),) + + +def reduce_failure(directory, seconds=120): + directory = Path(directory).resolve() + report = json.loads((directory / "results.json").read_text()) + if not report["divergences"]: + raise ValueError("campaign has no divergence to reduce") + failure = report["divergences"][0] + target = signature(failure) + manifest = json.loads((directory / "manifest.json").read_text()) + config, metadata = manifest["config"], manifest["model"] + abi = json.loads((directory / "abi.json").read_text()) + layout = json.loads((directory / "layout.json").read_text()) + name = failure["case"]["id"] + row = next(row for row in json.loads((directory / "values.json").read_text()) if row["name"] == name) + best = {k: int(row[k]) for k in config["variables"]} + scratch = directory / "reduced" + scratch.mkdir(exist_ok=True) + for path in directory.iterdir(): + if path.is_file() and path.name not in ("results.json", "values.json"): + shutil.copyfile(path, scratch / path.name) + (scratch / "out").mkdir(exist_ok=True) + deadline = time.monotonic() + seconds + attempts = 0 + for key in best: + candidates = sorted({0, 1, 2, best[key] // 2, best[key] & (best[key] - 1)}) + for value in candidates: + if value >= best[key] or time.monotonic() >= deadline: + continue + trial = {**best, key: value} + result = execute(scratch, [materialize(config, metadata, abi, layout, trial, name)]) + attempts += 1 + if result["divergences"] and signature(result["divergences"][0]) == target: + best = trial + final = execute(scratch, [materialize(config, metadata, abi, layout, best, name)]) + if not final["divergences"] or signature(final["divergences"][0]) != target: + raise ValueError("reduced input did not preserve the divergence") + write_json(scratch / "values.json", [{"name": name, **{k: str(v) for k, v in best.items()}}]) + reduced_cases = json.loads((scratch / "model-cases.json").read_text()) + manifest["casesSha256"] = hashlib.sha256(json.dumps(reduced_cases, sort_keys=True).encode()).hexdigest() + write_json(scratch / "manifest.json", manifest) + # Generated programs additionally admit a typed AST reduction. Do not edit + # arbitrary production Solidity by textual deletion. + program_path = directory.parent / "program.json" + if program_path.exists(): + spec = json.loads(program_path.read_text()) + for tree in smaller_expressions(spec["expression"]): + if time.monotonic() >= deadline: + break + trial = copy.deepcopy(spec) + trial["expression"] = tree + program_dir = scratch / "program" + fixture = write_program(program_dir, trial) + write_json(program_dir / "corpus.json", [{"name": name, **{k: str(v) for k, v in best.items()}}]) + result = campaign(fixture, program_dir / "run", 0, 0) + attempts += 1 + if result["divergences"] and signature(result["divergences"][0]) == target: + spec = trial + write_json(scratch / "minimal-program.json", spec) + (scratch / "minimal.sol").write_text((program_dir / "Slice.sol").read_text()) + return {"attempts": attempts, "output": str(scratch), "divergences": final["divergences"]} diff --git a/scripts/solidity_differential/requirements.txt b/scripts/solidity_differential/requirements.txt new file mode 100644 index 0000000000..2387783ce0 --- /dev/null +++ b/scripts/solidity_differential/requirements.txt @@ -0,0 +1,2 @@ +eth-abi==5.2.0 +eth-hash[pycryptodome]==0.7.1 diff --git a/scripts/solidity_slice_differential.py b/scripts/solidity_slice_differential.py new file mode 100755 index 0000000000..d05d3bbf14 --- /dev/null +++ b/scripts/solidity_slice_differential.py @@ -0,0 +1,63 @@ +#!/usr/bin/env python3 +"""Run reproducible A/B/C comparisons for a declarative Solidity slice fixture.""" +import argparse +import hashlib +import json +from pathlib import Path +import sys +from solidity_differential.engine import HarnessError, campaign, execute + + +def main(): + parser = argparse.ArgumentParser(description=__doc__) + parser.add_argument("--config", type=Path) + parser.add_argument("--output", type=Path, required=True) + parser.add_argument("--seed", type=int, default=2438) + parser.add_argument("--cases", type=int, default=128, help="generated cases in addition to the fixed corpus") + parser.add_argument("--replay", action="store_true", help="replay materialized cases in --output") + parser.add_argument("--programs", type=int, default=0, help="generate this many source programs, each with a renamed/helper variant") + parser.add_argument("--reduce", action="store_true", help="reduce the first recorded divergence") + parser.add_argument("--reduce-seconds", type=int, default=120) + parser.add_argument("--mutations", action="store_true", help="test importer and Denote mutants in isolated snapshots") + parser.add_argument("--mutant", action="append", help="select a named mutant") + args = parser.parse_args() + if args.cases < 0 or args.programs < 0 or args.reduce_seconds <= 0: + parser.error("case/program counts must be nonnegative and reduction budget positive") + if sum(map(bool, (args.config, args.replay, args.programs, args.reduce, args.mutations))) != 1: + parser.error("choose exactly one of --config, --replay, --programs, --reduce, --mutations") + if args.mutant and not args.mutations: + parser.error("--mutant requires --mutations") + try: + if args.mutations: + from solidity_differential.mutations import mutation_campaign + report = mutation_campaign(args.output, args.mutant) + elif args.reduce: + from solidity_differential.reduce import reduce_failure + report = reduce_failure(args.output, args.reduce_seconds) + elif args.programs: + from solidity_differential.programs import generated_campaign + report = generated_campaign(args.output, args.programs, args.cases, args.seed) + elif args.replay: + cases = json.loads((args.output / "model-cases.json").read_text()) + manifest = json.loads((args.output / "manifest.json").read_text()) + if hashlib.sha256(json.dumps(cases, sort_keys=True).encode()).hexdigest() != manifest["casesSha256"]: + raise HarnessError("replay cases changed") + report = execute(args.output, cases) + else: + if not args.config: + parser.error("--config is required unless replaying") + report = campaign(args.config, args.output.resolve(), args.cases, args.seed) + except (HarnessError, ValueError, KeyError, OSError) as error: + print(f"HARNESS ERROR: {error}", file=sys.stderr) + return 2 + print(json.dumps({k: v for k, v in report.items() if k != "divergences"})) + if args.reduce: + return 0 # success means the original divergence was preserved + if report["divergences"]: + print(f"DIVERGENCE: inspect the reports in {args.output}", file=sys.stderr) + return 1 + return 0 + + +if __name__ == "__main__": + sys.exit(main()) diff --git a/scripts/solidity_slice_mutations.py b/scripts/solidity_slice_mutations.py index e6afff3867..eb5606552b 100644 --- a/scripts/solidity_slice_mutations.py +++ b/scripts/solidity_slice_mutations.py @@ -192,11 +192,11 @@ def helper_local_collision(dest: Path) -> None: def generated_name_collision(dest: Path) -> None: path = dest / "Slice.sol" - path.write_text(path.read_text().replace("credit = p.credit", "sliceTmp0 = p.credit") - .replace("credit.mulDivDown", "sliceTmp0.mulDivDown") - .replace("credit > 0", "sliceTmp0 > 0") - .replace("credit - post", "sliceTmp0 - post") - .replace(", credit)", ", sliceTmp0)")) + path.write_text(path.read_text().replace("credit = p.credit", "_verity_slice_tmp_0 = p.credit") + .replace("credit.mulDivDown", "_verity_slice_tmp_0.mulDivDown") + .replace("credit > 0", "_verity_slice_tmp_0 > 0") + .replace("credit - post", "_verity_slice_tmp_0 - post") + .replace(", credit)", ", _verity_slice_tmp_0)")) def unrelated_layout(dest: Path) -> None: @@ -292,7 +292,9 @@ def main() -> None: expect_failure("field-detected", write_project("field", change_field), "witness changed") expect_failure("return-order", write_project("returns", swap_returns), "witness changed") expect_failure("layout", write_project("layout", swap_layout), "witness changed") - expect_failure("reachable-for", write_project("reachable", call_unused), "unsupported") + diagnostic = expect_failure("reachable-for", write_project("reachable", call_unused), "unsupported") + assert "[solidity-slice:unsupported]" in diagnostic and "closure:" in diagnostic + assert "Lib.sol:" in diagnostic and "closure: C.f -> L.unused" in diagnostic, diagnostic if digest_of(base) == digest_of(rounding): raise SystemExit("helper change did not move the digest") print("pass helper-digest") diff --git a/scripts/test_solidity_differential.py b/scripts/test_solidity_differential.py new file mode 100644 index 0000000000..9554b1731b --- /dev/null +++ b/scripts/test_solidity_differential.py @@ -0,0 +1,78 @@ +"""Protocol, determinism and failure classification tests (stdlib only).""" +import json +from pathlib import Path +import tempfile +import unittest +from unittest.mock import patch + +from solidity_differential.cases import generate, canonical +from solidity_differential.engine import HarnessError, parse_evm, execute +from solidity_differential.programs import source, smaller_expressions +from solidity_differential.reduce import signature + + +class SolidityDifferentialTests(unittest.TestCase): + def test_seed_reproduces_every_input(self): + config = {"variables": {"a": 8, "b": 16}, "ordered_pairs": []} + first = generate(config, 100, 17, []) + self.assertEqual(first, generate(config, 100, 17, [])) + self.assertNotEqual(first, generate(config, 100, 18, [])) + self.assertTrue(all(0 <= v["a"] < 256 and 0 <= v["b"] < 65536 for _, v in first)) + self.assertEqual(len({name for name, _ in first}), 100) + + def test_corpus_and_defaults_are_preserved(self): + result = generate({"variables": {"id": 256, "x": 8}, "defaults": {"id": 7}}, 0, 0, [{"name": "A", "x": "3"}]) + self.assertEqual(result, [("A", {"id": 7, "x": 3})]) + + def test_nested_abi_signature(self): + self.assertEqual(canonical({"type": "tuple[]", "components": [{"type": "uint256"}, {"type": "address"}]}), "(uint256,address)[]") + + def test_missing_bad_or_truncated_results_are_errors(self): + with tempfile.TemporaryDirectory() as directory: + path = Path(directory) / "results" + for text in ("", "0 timeout 0x\n", "1 revert 0x\n", "0 ok 0x01\n", "0 revert 0x 1\n"): + path.write_text(text) + with self.assertRaises(HarnessError): + parse_evm(path, [{"id": "A", "observe": []}]) + path.write_text("0 revert 0x\n") + self.assertEqual(parse_evm(path, [{"id": "A", "observe": []}])[0]["status"], "revert") + + def test_empty_or_stale_campaign_cannot_pass(self): + with tempfile.TemporaryDirectory() as directory: + path = Path(directory) + (path / "manifest.json").write_text(json.dumps({"implementationHashes": {}})) + with patch("solidity_differential.engine.implementation_hashes", return_value={"changed": "x"}): + with self.assertRaisesRegex(HarnessError, "implementation changed"): + execute(path, []) + with patch("solidity_differential.engine.implementation_hashes", return_value={}): + with self.assertRaisesRegex(HarnessError, "empty campaign"): + execute(path, []) + + def test_modified_bytecode_is_rejected_before_execution(self): + with tempfile.TemporaryDirectory() as directory: + path = Path(directory) + (path / "source.bin").write_bytes(b"changed") + (path / "manifest.json").write_text(json.dumps({ + "implementationHashes": {}, "artifactHashes": {"source.bin": "original"}})) + with patch("solidity_differential.engine.implementation_hashes", return_value={}): + with self.assertRaisesRegex(HarnessError, "campaign artifact changed"): + execute(path, [{"id": "A"}]) + + def test_program_reduction_preserves_typed_tree(self): + tree = {"kind": "binary", "op": "*", "left": {"kind": "x"}, "right": {"kind": "y"}} + self.assertIn({"kind": "x"}, list(smaller_expressions(tree))) + rendered = source({"bits": 248, "expression": tree, "helper": True, "renamed": False, "projection_collision": True}) + self.assertIn("uint248(x)", rendered) + self.assertIn("_verity_slice.tmp_0", rendered) + self.assertIn("L.work", rendered) + + def test_reducer_distinguishes_status_and_return_mismatches(self): + a = {"status": "ok", "words": ["1"], "storage": [], "data": "0x01"} + wrong_return = {**a, "words": ["2"]} + reverted = {**a, "status": "revert", "words": []} + self.assertNotEqual(signature({"source": a, "model": wrong_return, "compiled": a}), + signature({"source": a, "model": reverted, "compiled": a})) + + +if __name__ == "__main__": + unittest.main() diff --git a/scripts/verify_sync_spec.json b/scripts/verify_sync_spec.json index a46f27c76a..6557fc9713 100644 --- a/scripts/verify_sync_spec.json +++ b/scripts/verify_sync_spec.json @@ -23,6 +23,11 @@ "packages/**", "lakefile.lean", "lake-manifest.json", + "scripts/solidity_differential/**", + "scripts/solidity_slice_differential.py", + "scripts/check_solidity_differential.sh", + "scripts/solidity_slice_mutations.py", + "scripts/setup_solc_slice.py", "lean-toolchain" ], "compiler_paths": [ @@ -48,6 +53,11 @@ "scripts/property_utils.py", "lakefile.lean", "lake-manifest.json", + "scripts/solidity_differential/**", + "scripts/solidity_slice_differential.py", + "scripts/check_solidity_differential.sh", + "scripts/solidity_slice_mutations.py", + "scripts/setup_solc_slice.py", "lean-toolchain", "foundry.toml" ], @@ -910,6 +920,9 @@ "static-gas-report", "static-gas-report-patched" ], + "compiler-regressions": [ + "solidity-slice-differential" + ], "lean-profile": [ "lean-perf-queue" ] @@ -933,6 +946,9 @@ "gas-report-static.tsv", "gas-report-static-patched.tsv" ], + "compiler-regressions": [ + ".lake/slice-differential/**\n!.lake/slice-differential/**/.lake/build/**\n!.lake/slice-differential/**/.lake/packages/**" + ], "lean-profile": [ "lean-perf-queue.md" ] diff --git a/scripts/verify_sync_spec_source.py b/scripts/verify_sync_spec_source.py index 9f8c59a7ce..d61924b73e 100644 --- a/scripts/verify_sync_spec_source.py +++ b/scripts/verify_sync_spec_source.py @@ -932,3 +932,12 @@ def build_spec() -> dict: return copy.deepcopy(SPEC) + +# Solidity slice differential changes require the compiled-model validation lane. +for _lane in ("build_paths", "compiler_paths"): + SPEC[_lane][SPEC[_lane].index("lean-toolchain"):SPEC[_lane].index("lean-toolchain")] = ['scripts/solidity_differential/**', 'scripts/solidity_slice_differential.py', 'scripts/check_solidity_differential.sh', 'scripts/solidity_slice_mutations.py', 'scripts/setup_solc_slice.py'] +SPEC["expected_uploaded_artifacts"]["compiler-regressions"] = ["solidity-slice-differential"] +SPEC["expected_uploaded_artifact_paths"]["compiler-regressions"] = [".lake/slice-differential/**\n!.lake/slice-differential/**/.lake/build/**\n!.lake/slice-differential/**/.lake/packages/**"] +for _key in ("expected_uploaded_artifacts", "expected_uploaded_artifact_paths"): + _profile = SPEC[_key].pop("lean-profile") + SPEC[_key]["lean-profile"] = _profile From 37eb5ef142947d41917c27ed91defb1abad16de8 Mon Sep 17 00:00:00 2001 From: Thomas Marchand Date: Thu, 24 Sep 2026 14:17:17 +0200 Subject: [PATCH 09/22] Record metamorphic slice divergences as campaign evidence A mismatch between renamed/helper variants of a generated program failed the run without leaving any artifact, and --reduce then crashed on the missing case. Write metamorphic-divergence.json with the differing source rows and have the reducer reject such failures explicitly. --- Compiler/SoliditySlice/TESTING.md | 4 +++- scripts/solidity_differential/programs.py | 8 ++++++-- scripts/solidity_differential/reduce.py | 2 ++ scripts/test_solidity_differential.py | 22 ++++++++++++++++++++-- 4 files changed, 31 insertions(+), 5 deletions(-) diff --git a/Compiler/SoliditySlice/TESTING.md b/Compiler/SoliditySlice/TESTING.md index 972641bd91..e4bd44cce1 100644 --- a/Compiler/SoliditySlice/TESTING.md +++ b/Compiler/SoliditySlice/TESTING.md @@ -58,7 +58,9 @@ Each generated expression is run directly and through hygienic renamed/helper variants, including a scalar projection whose name collides with the temporary prefix. The grammar exercises uint8/16/128/248/256 casts, checked arithmetic, ternaries and helper composition. Independent source executions must agree -between metamorphic variants as well as between A/B/C. +between metamorphic variants as well as between A/B/C. A metamorphic mismatch +is recorded in `metamorphic-divergence.json` with the differing source rows; +it is investigated directly rather than by the input reducer. ## Failure evidence and reduction diff --git a/scripts/solidity_differential/programs.py b/scripts/solidity_differential/programs.py index 914998a1e6..0072b729c2 100644 --- a/scripts/solidity_differential/programs.py +++ b/scripts/solidity_differential/programs.py @@ -106,6 +106,10 @@ def generated_campaign(output, count, cases, seed): # Renaming + helper extraction must preserve the observed behavior. rows = (directory / "run/out/source.txt").read_text() if reference is not None and reference != rows: - return {"programs": len(reports), "divergences": [{"metamorphic": str(directory)}]} - reference = rows + divergence = {"metamorphic": str(directory), "reference": str(reference_dir), + "rows": [{"line": k, "reference": a, "variant": b} for k, (a, b) + in enumerate(zip(reference.splitlines(), rows.splitlines())) if a != b]} + write_json(output / "metamorphic-divergence.json", divergence) + return {"programs": len(reports), "divergences": [divergence]} + reference, reference_dir = rows, directory return {"programs": len(reports), "cases": sum(r["cases"] for r in reports), "divergences": []} diff --git a/scripts/solidity_differential/reduce.py b/scripts/solidity_differential/reduce.py index fdc035ec7e..7f4df81590 100644 --- a/scripts/solidity_differential/reduce.py +++ b/scripts/solidity_differential/reduce.py @@ -25,6 +25,8 @@ def reduce_failure(directory, seconds=120): if not report["divergences"]: raise ValueError("campaign has no divergence to reduce") failure = report["divergences"][0] + if "case" not in failure: + raise ValueError("only A/B/C case divergences are reducible; inspect metamorphic-divergence.json") target = signature(failure) manifest = json.loads((directory / "manifest.json").read_text()) config, metadata = manifest["config"], manifest["model"] diff --git a/scripts/test_solidity_differential.py b/scripts/test_solidity_differential.py index 9554b1731b..dd4e81931b 100644 --- a/scripts/test_solidity_differential.py +++ b/scripts/test_solidity_differential.py @@ -7,8 +7,8 @@ from solidity_differential.cases import generate, canonical from solidity_differential.engine import HarnessError, parse_evm, execute -from solidity_differential.programs import source, smaller_expressions -from solidity_differential.reduce import signature +from solidity_differential.programs import generated_campaign, source, smaller_expressions +from solidity_differential.reduce import reduce_failure, signature class SolidityDifferentialTests(unittest.TestCase): @@ -73,6 +73,24 @@ def test_reducer_distinguishes_status_and_return_mismatches(self): self.assertNotEqual(signature({"source": a, "model": wrong_return, "compiled": a}), signature({"source": a, "model": reverted, "compiled": a})) + def test_metamorphic_divergence_is_recorded_and_not_reduced(self): + def fake_campaign(fixture, run, cases, seed): + out = Path(run) / "out" + out.mkdir(parents=True, exist_ok=True) + rows = "0 ok 0x01\n" if "program-0-0" in str(run) else "0 revert 0x\n" + (out / "source.txt").write_text(rows) + return {"cases": 1, "divergences": []} + + with tempfile.TemporaryDirectory() as directory: + with patch("solidity_differential.programs.campaign", fake_campaign): + report = generated_campaign(directory, 1, 0, 0) + evidence = json.loads((Path(directory) / "metamorphic-divergence.json").read_text()) + self.assertEqual(report["divergences"], [evidence]) + self.assertEqual(evidence["rows"], [{"line": 0, "reference": "0 ok 0x01", "variant": "0 revert 0x"}]) + (Path(directory) / "results.json").write_text(json.dumps(report)) + with self.assertRaisesRegex(ValueError, "metamorphic"): + reduce_failure(directory) + if __name__ == "__main__": unittest.main() From d9c47797732d3c495201365dff3d1b6908fa2d79 Mon Sep 17 00:00:00 2001 From: Thomas Marchand Date: Thu, 24 Sep 2026 15:19:13 +0200 Subject: [PATCH 10/22] Remove the proof-only Solidity Vault importer The `solidity_contract` importer elaborated Solidity into its own `Sol.Fn` semantics, separate from CompilationModel, so none of its theorems fed the compiler proofs. The CompilationModel slice importer (validated A/B/C against solc and the compiled model) is now the single Solidity front end. Delete Contracts/VaultFromSolidity, the SolidityImportSmoke inheritance, modifiers and structs smokes, `solidity_simp`, the 0.8.33 importer setup script and CI step, the Vault Foundry test, the Pareto import roadmap, and the now-empty named-storage spec lint. Drop the matching lakefile targets, exemptions, property manifest entries and documentation sections, and regenerate PrintAxioms and the verification status (356 -> 329 theorems). --- .github/workflows/verify.yml | 5 - AUDIT.md | 71 +- AXIOMS.md | 19 - .../Inheritance/Inheritance.lean | 7 - .../Inheritance/Inheritance.sol | 58 - .../Inheritance/Proofs.lean | 123 -- .../SolidityImportSmoke/Inheritance/Spec.lean | 44 - .../Inheritance/scripts/inheritance_test.py | 381 ---- .../Modifiers/Modifiers.lean | 7 - .../Modifiers/Modifiers.sol | 93 - .../SolidityImportSmoke/Modifiers/Proofs.lean | 75 - .../SolidityImportSmoke/Modifiers/Spec.lean | 36 - .../Modifiers/scripts/modifiers_test.py | 268 --- .../SolidityImportSmoke/Structs/Proofs.lean | 39 - .../SolidityImportSmoke/Structs/Spec.lean | 17 - .../SolidityImportSmoke/Structs/Structs.lean | 7 - .../SolidityImportSmoke/Structs/Structs.sol | 32 - .../Structs/scripts/structs_test.py | 293 --- .../VaultFromSolidity/Importer/Importer.lean | 1915 ----------------- .../VaultFromSolidity/Importer/Semantics.lean | 244 --- .../VaultFromSolidity/Importer/Syntax.lean | 253 --- .../scripts/solidity_importer_test.py | 591 ----- .../Proofs/ExecutionProof.lean | 153 -- Contracts/VaultFromSolidity/Spec.lean | 68 - Contracts/VaultFromSolidity/Vault.sol | 42 - .../VaultFromSolidity/VaultFromSolidity.lean | 7 - Makefile | 8 +- PrintAxioms.lean | 46 +- README.md | 76 - TRUST_ASSUMPTIONS.md | 127 +- Verity/Proofs/Stdlib/SolidityImport.lean | 51 - artifacts/trust_surface_report.json | 2 +- artifacts/verification_status.json | 22 +- docs-site/public/llms.txt | 8 +- docs/SOLIDITY_IMPORT_ROADMAP.md | 849 -------- docs/VERIFICATION_STATUS.md | 14 +- lakefile.lean | 69 - scripts/REFERENCE.md | 4 +- scripts/check_contract_structure.py | 6 - scripts/check_solc_pin.py | 30 - scripts/check_spec_named_storage.py | 133 -- scripts/consolidation-inventory.md | 1 - scripts/generate_print_axioms.py | 3 - scripts/lean_lint.py | 1 - scripts/setup_solc_importer.py | 98 - scripts/setup_solc_slice.py | 3 +- scripts/test_check_spec_named_storage.py | 124 -- scripts/test_lean_lint.py | 1 - scripts/verify_sync_spec.json | 1 - scripts/verify_sync_spec_source.py | 1 - test/Vault.t.sol | 99 - test/property_exclusions.json | 31 - test/property_manifest.json | 31 - 53 files changed, 27 insertions(+), 6660 deletions(-) delete mode 100644 Contracts/SolidityImportSmoke/Inheritance/Inheritance.lean delete mode 100644 Contracts/SolidityImportSmoke/Inheritance/Inheritance.sol delete mode 100644 Contracts/SolidityImportSmoke/Inheritance/Proofs.lean delete mode 100644 Contracts/SolidityImportSmoke/Inheritance/Spec.lean delete mode 100644 Contracts/SolidityImportSmoke/Inheritance/scripts/inheritance_test.py delete mode 100644 Contracts/SolidityImportSmoke/Modifiers/Modifiers.lean delete mode 100644 Contracts/SolidityImportSmoke/Modifiers/Modifiers.sol delete mode 100644 Contracts/SolidityImportSmoke/Modifiers/Proofs.lean delete mode 100644 Contracts/SolidityImportSmoke/Modifiers/Spec.lean delete mode 100755 Contracts/SolidityImportSmoke/Modifiers/scripts/modifiers_test.py delete mode 100644 Contracts/SolidityImportSmoke/Structs/Proofs.lean delete mode 100644 Contracts/SolidityImportSmoke/Structs/Spec.lean delete mode 100644 Contracts/SolidityImportSmoke/Structs/Structs.lean delete mode 100644 Contracts/SolidityImportSmoke/Structs/Structs.sol delete mode 100755 Contracts/SolidityImportSmoke/Structs/scripts/structs_test.py delete mode 100644 Contracts/VaultFromSolidity/Importer/Importer.lean delete mode 100644 Contracts/VaultFromSolidity/Importer/Semantics.lean delete mode 100644 Contracts/VaultFromSolidity/Importer/Syntax.lean delete mode 100644 Contracts/VaultFromSolidity/Importer/scripts/solidity_importer_test.py delete mode 100644 Contracts/VaultFromSolidity/Proofs/ExecutionProof.lean delete mode 100644 Contracts/VaultFromSolidity/Spec.lean delete mode 100644 Contracts/VaultFromSolidity/Vault.sol delete mode 100644 Contracts/VaultFromSolidity/VaultFromSolidity.lean delete mode 100644 Verity/Proofs/Stdlib/SolidityImport.lean delete mode 100644 docs/SOLIDITY_IMPORT_ROADMAP.md delete mode 100644 scripts/check_spec_named_storage.py delete mode 100644 scripts/setup_solc_importer.py delete mode 100644 scripts/test_check_spec_named_storage.py delete mode 100644 test/Vault.t.sol diff --git a/.github/workflows/verify.yml b/.github/workflows/verify.yml index 4bdd3eb50b..b0d6977d77 100644 --- a/.github/workflows/verify.yml +++ b/.github/workflows/verify.yml @@ -401,11 +401,6 @@ jobs: disable-lake-cache-restore: ${{ env.VERIFY_DISABLE_LAKE_CACHE_RESTORE }} cache-primary-key: lake-${{ runner.os }}-${{ hashFiles('lean-toolchain') }}-${{ hashFiles('lakefile.lean') }}-${{ hashFiles('lake-manifest.json') }}-${{ github.run_id }} - - name: Setup pinned solc for Lean Solidity importer - uses: ./.github/actions/setup-solc - with: - destination: .lake/solidity-import/solc - - name: Rebuild cached local Lean modules run: | rm -rf .lake/build/lib/lean/Verity .lake/build/ir/Verity diff --git a/AUDIT.md b/AUDIT.md index 32ea98a448..7a712426fa 100644 --- a/AUDIT.md +++ b/AUDIT.md @@ -18,8 +18,7 @@ imported body: `(95, 5, 5)` and a revert. `smoke_step_credit` is a kernel proof of the first packed `credit` read. It is not a proof of the whole body, and it is not a proof that the model matches solc bytecode. -The slice compiler pin is solc `0.8.34+commit.80d5c536`, installed separately -from the Vault 0.8.33 pin by `python3 scripts/setup_solc_slice.py`. The source +The slice compiler pin is solc `0.8.34+commit.80d5c536`, installed by `python3 scripts/setup_solc_slice.py`. The source digest reads the importer modules from the Verity tree, or from `.lake/packages/verity` when a downstream package elaborates the command. The solc binary remains the elaborating package's `.lake/solidity-import/solc-0.8.34`. @@ -39,74 +38,6 @@ actual imported execution, with arbitrary state/oracle and mathematical integer ghosts. That proof adds no source-to-model equivalence claim. See `Compiler/SoliditySlice/README.md` for the supported subset and freshness checks. -## Proof-only Solidity Vault POC - -The focused suite probes unknown, wrong-typed, and missing AST fields (including -documentation metadata), invalid source spans, and malformed storage layout -through synthetic compiler-output mutations, plus unsupported source constructs, -contract `layout at`, -registered-source symlink escape, and Lean importer digest sensitivity. It also -checks safe transparent declarations, duplicate aliases, a deliberately -malformed late declaration and complete registration rollback (including the -named storage view), plus the pinned compiler's checksum. Named-storage checks -cover `v.totalAssets` dot notation and `#print view`, a -declaration-reorder mutation that moves solc slots while every proof still -builds, a state-variable rename that makes `Spec.lean` fail to elaborate, and -Solidity variables named `Storage` or `step` rejected with a source position. -Behaviour mutations must break both the `*_success_spec` theorem and the -`*_meets_spec` theorem of the affected entry point, three `Spec.lean` mutations -that weaken a promise must fail inside both corresponding theorems, and a new -`mint` entry point must break `solvent_invariant`. The `spec_named_storage` -lean_lint rule (in `make check`) rejects every raw `ContractState` accessor -(the list is read from `Verity/Core.lean`), raw storage fields, direct -`ContractState` mentions, positional projections, and `knownAddresses` in -opted-in spec files. The digest scope is documented in `TRUST_ASSUMPTIONS.md`; it is not a transitive build identity. - -The importer accepts the official 0.8.33 linux-amd64 and macosx-amd64 -builds published on `binaries.soliditylang.org` (the same lists `solc-select` -uses). Checksum verification uses `/usr/bin/sha256sum` on Linux and -`/usr/bin/shasum` on macOS against that committed allowlist. `sourceDigest` -hashes source, standard-json, importer text, and `0.8.33+commit.64118f21`, not -the host binary. Setup and `make check-solc-published` may fetch `list.json`; -Lake does not. Linux CI still installs and runs the linux-amd64 artifact. - -Evidence command: -`python3 Contracts/VaultFromSolidity/Importer/scripts/solidity_importer_test.py` -(after `lake build VaultFromSolidity` and installation of the pinned compiler). -The focused runner builds and audits the imported execution proofs, changes -accepted deposit/getter behavior while preserving source mtime and requires old -proofs to fail at both the success and spec layer, weakens the named spec -and requires both corresponding theorems to fail, rejects unsupported source, -checks unchanged artifacts, and -exercises Lean-importer and compiler content invalidation. Mutations occur only -in disposable copies. This is local acceptance evidence, not a new CI job, -bytecode/runtime test, or proof of translation correctness. - -The complete example surface lives under `Contracts/VaultFromSolidity`: Solidity -source, Lean importer, specification, execution proofs and focused acceptance -tests. It is independent of the handwritten `Contracts/Vault` example. The S1 -inheritance slice adds `Contracts/SolidityImportSmoke/Inheritance` with the same -shape (source, import command, named-storage spec, proofs, focused Python -suite). The S2 modifiers/structs slice adds -`Contracts/SolidityImportSmoke/Modifiers` and -`Contracts/SolidityImportSmoke/Structs` with the same shape. No -Python frontend, custom serialized IR, generated Lean source, or bytecode is in -the translation path: the accepted Solidity subset is the kernel-checked -inductive in `Importer/Syntax.lean` and `Importer/Semantics.lean` is its single -meaning. Trust and axiom scope are recorded in -`TRUST_ASSUMPTIONS.md` and `AXIOMS.md`. - -Evidence command for the inheritance slice: -`python3 Contracts/SolidityImportSmoke/Inheritance/scripts/inheritance_test.py` -(after `lake build SolidityImportSmokeInheritance`). -Evidence commands for the modifiers/structs slice: -`python3 Contracts/SolidityImportSmoke/Modifiers/scripts/modifiers_test.py` -(after `lake build SolidityImportSmokeModifiers`) and -`python3 Contracts/SolidityImportSmoke/Structs/scripts/structs_test.py` -(after `lake build SolidityImportSmokeStructs`). The structs suite also -imports a named constructor that lists `{who, amount}` out of member order -(`Expr.pairRev` evaluates source order then packs `uint256 × address`). - ## Current Audit State - Lean proof placeholders: 0 `sorry` in compiler/proof modules. diff --git a/AXIOMS.md b/AXIOMS.md index 8f34916c4e..7bafd04a33 100644 --- a/AXIOMS.md +++ b/AXIOMS.md @@ -11,25 +11,6 @@ concrete packed read. Interpreter `#eval` checks are not theorems. The frontend builds the model from solc JSON remains a trust assumption; see `TRUST_ASSUMPTIONS.md`. -## Proof-only Solidity Vault audit - -`PrintAxioms.lean` includes the imported Vault execution theorems. The focused -`solidity_importer_test.py` runs `#print axioms` in a disposable audit module for -every theorem in `Contracts/VaultFromSolidity/Proofs/ExecutionProof.lean` and requires -coverage of all declared theorems, rejecting `sorryAx` and project axioms. -Its malformed-declaration probe also checks that kernel error recovery does not -leave any partial declarations or fallback axioms in the import namespace. -The imported Vault proofs report only the standard Lean foundations `propext` -and `Quot.sound`; they do not depend on `solidityMappingSlot_injective`. -`solvent_invariant` is included in that audit. The inheritance-slice suite -`inheritance_test.py` runs the same `#print axioms` gate on every theorem in -`Contracts/SolidityImportSmoke/Inheritance/Proofs.lean` and likewise requires -only `propext` and `Quot.sound`. The modifiers and structs suites -`modifiers_test.py` and `structs_test.py` run the same gate on -`Contracts/SolidityImportSmoke/Modifiers/Proofs.lean` and -`Contracts/SolidityImportSmoke/Structs/Proofs.lean`. This does not remove the trusted Solidity frontend/translation boundary described -in `TRUST_ASSUMPTIONS.md`, or change the compiler axiom registry below. - ## Policy Axioms are exceptional. When an axiom exists, it must have: diff --git a/Contracts/SolidityImportSmoke/Inheritance/Inheritance.lean b/Contracts/SolidityImportSmoke/Inheritance/Inheritance.lean deleted file mode 100644 index ca5f22250c..0000000000 --- a/Contracts/SolidityImportSmoke/Inheritance/Inheritance.lean +++ /dev/null @@ -1,7 +0,0 @@ -import Contracts.VaultFromSolidity.Importer.Importer - -namespace Contracts.SolidityImportSmoke.Inheritance - -solidity_contract Child from "Inheritance.sol" - -end Contracts.SolidityImportSmoke.Inheritance diff --git a/Contracts/SolidityImportSmoke/Inheritance/Inheritance.sol b/Contracts/SolidityImportSmoke/Inheritance/Inheritance.sol deleted file mode 100644 index 6df6cad19f..0000000000 --- a/Contracts/SolidityImportSmoke/Inheritance/Inheritance.sol +++ /dev/null @@ -1,58 +0,0 @@ -// SPDX-License-Identifier: MIT -pragma solidity ^0.8.33; - -/// Synthetic Storage-like / Pausable-like / Ownable-like bases plus a diamond, -/// used as the S1 inheritance smoke. Only POC types plus `address` scalars. -abstract contract Base { - uint256 public baseValue; - - function _bump() internal virtual { - baseValue += 1; - } -} - -abstract contract Left is Base { - uint256 public leftValue; -} - -abstract contract Right is Base { - uint256 public rightValue; -} - -abstract contract PausableLike { - uint256 public paused; - - function _pause() internal virtual { - paused = 1; - } - - function pause() external { - _pause(); - } -} - -abstract contract OwnableLike { - address public owner; -} - -contract Child is Left, Right, PausableLike, OwnableLike { - uint256 public childValue; - bytes32 private unusedGap; - - function _pause() internal virtual override { - super._pause(); - childValue += 1; - } - - function go() external { - _pause(); - } - - function bump() external { - _bump(); - } - - function _add(uint256 a, uint256 b) internal pure returns (uint256) { - return a + b; - } -} diff --git a/Contracts/SolidityImportSmoke/Inheritance/Proofs.lean b/Contracts/SolidityImportSmoke/Inheritance/Proofs.lean deleted file mode 100644 index 3d77c57f12..0000000000 --- a/Contracts/SolidityImportSmoke/Inheritance/Proofs.lean +++ /dev/null @@ -1,123 +0,0 @@ -import Contracts.SolidityImportSmoke.Inheritance.Spec -import Verity.Proofs.Stdlib.SolidityImport - -namespace Contracts.SolidityImportSmoke.Inheritance.Proofs -open Verity -open Verity.Stdlib.Math -open Verity.Core.Invariant -open Verity.Proofs.Stdlib.SolidityImport -open Contracts.SolidityImportSmoke.Inheritance.Child -open Spec - -theorem pause_success_spec (s post : ContractState) - (h : PausableLike_pause.run s = .success () post) : - pause_spec (view s) (view post) := by - unfold pause_spec - by_cases h0 : s.msgValue = 0 - · by_cases hcv : (view s).childValue.val + 1 ≤ Verity.Core.MAX_UINT256 - · solidity_simp - subst h - solidity_simp - · solidity_simp - · solidity_simp - -theorem go_success_spec (s post : ContractState) - (h : go.run s = .success () post) : - pause_spec (view s) (view post) := by - unfold pause_spec - by_cases h0 : s.msgValue = 0 - · by_cases hcv : (view s).childValue.val + 1 ≤ Verity.Core.MAX_UINT256 - · solidity_simp - subst h - solidity_simp - · solidity_simp - · solidity_simp - -theorem bump_success_spec (s post : ContractState) - (h : bump.run s = .success () post) : - bump_spec (view s) (view post) := by - unfold bump_spec - by_cases h0 : s.msgValue = 0 - · by_cases hb : (view s).baseValue.val + 1 ≤ Verity.Core.MAX_UINT256 - · solidity_simp - subst h - solidity_simp - · solidity_simp - · solidity_simp - -/-- A base entry point that calls the virtual observes the child's override. -/ -theorem dispatch_is_child (s post : ContractState) - (h : PausableLike_pause.run s = .success () post) : - (view post).paused = 1 ∧ (view post).childValue = (view s).childValue + 1 := by - have hspec := pause_success_spec s post h - exact ⟨hspec.1, hspec.2.1⟩ - -/-- `go()` calls `Child._pause`, whose `super._pause()` runs the parent body. -/ -theorem super_runs_parent (s post : ContractState) - (h : go.run s = .success () post) : - (view post).paused = 1 := - (go_success_spec s post h).1 - -theorem pause_meets_spec (s : ContractState) - (h0 : s.msgValue = 0) (hfits : pauseFits (view s)) : - ∃ post, PausableLike_pause.run s = ContractResult.success () post ∧ - pause_spec (view s) (view post) := by - unfold pause_spec - solidity_simp [pauseFits] - -theorem go_meets_spec (s : ContractState) - (h0 : s.msgValue = 0) (hfits : pauseFits (view s)) : - ∃ post, go.run s = ContractResult.success () post ∧ - pause_spec (view s) (view post) := by - unfold pause_spec - solidity_simp [pauseFits] - -theorem bump_meets_spec (s : ContractState) - (h0 : s.msgValue = 0) (hfits : bumpFits (view s)) : - ∃ post, bump.run s = ContractResult.success () post ∧ - bump_spec (view s) (view post) := by - unfold bump_spec - solidity_simp [bumpFits] - -/-- `step` is go, bump, PausableLike_pause, then getters in layout order. -/ -theorem paused_invariant : PreservedBy (fun s => pausedFlag (view s)) step := by - intro s s' hflag hstep - rcases hstep with hgo | hbump | hpause | hbase | hleft | hright | hpaused | howner | hchild - · subst hgo - cases run_snd_cases go s with - | inl hrev => rw [hrev]; exact hflag - | inr hsucc => - rcases hsucc with ⟨_, post, hrun, hsnd⟩ - rw [hsnd] - exact Or.inr (go_success_spec s post hrun).1 - · subst hbump - cases run_snd_cases bump s with - | inl hrev => rw [hrev]; exact hflag - | inr hsucc => - rcases hsucc with ⟨_, post, hrun, hsnd⟩ - rw [hsnd] - have hp := (bump_success_spec s post hrun).2.2.1 - unfold pausedFlag at hflag ⊢ - rw [hp] - exact hflag - · subst hpause - cases run_snd_cases PausableLike_pause s with - | inl hrev => rw [hrev]; exact hflag - | inr hsucc => - rcases hsucc with ⟨_, post, hrun, hsnd⟩ - rw [hsnd] - exact Or.inr (pause_success_spec s post hrun).1 - · subst hbase - by_cases h0 : s.msgValue = 0 <;> solidity_simp [pausedFlag] - · subst hleft - by_cases h0 : s.msgValue = 0 <;> solidity_simp [pausedFlag] - · subst hright - by_cases h0 : s.msgValue = 0 <;> solidity_simp [pausedFlag] - · subst hpaused - by_cases h0 : s.msgValue = 0 <;> solidity_simp [pausedFlag] - · subst howner - by_cases h0 : s.msgValue = 0 <;> solidity_simp [pausedFlag] - · subst hchild - by_cases h0 : s.msgValue = 0 <;> solidity_simp [pausedFlag] - -end Contracts.SolidityImportSmoke.Inheritance.Proofs diff --git a/Contracts/SolidityImportSmoke/Inheritance/Spec.lean b/Contracts/SolidityImportSmoke/Inheritance/Spec.lean deleted file mode 100644 index 11ba8ffb33..0000000000 --- a/Contracts/SolidityImportSmoke/Inheritance/Spec.lean +++ /dev/null @@ -1,44 +0,0 @@ -import Verity.EVM.Uint256 -import Contracts.SolidityImportSmoke.Inheritance.Inheritance - -/-! -Named-storage promises for the S1 inheritance smoke. `paused` is a 0/1 flag -set by `_pause`; `childValue` counts how many times the child's override ran. --/ - -namespace Contracts.SolidityImportSmoke.Inheritance.Spec - -open Verity -open Verity.EVM.Uint256 -open Contracts.SolidityImportSmoke.Inheritance.Child - -/-- `paused` is a boolean encoded as `uint256` 0 or 1. -/ -def pausedFlag (v : Storage) : Prop := - v.paused = 0 ∨ v.paused = 1 - -/-- `pause()` / `go()` run the child's `_pause` override: parent sets `paused` -and the child increments `childValue`. -/ -def pause_spec (pre post : Storage) : Prop := - post.paused = 1 ∧ - post.childValue = pre.childValue + 1 ∧ - post.baseValue = pre.baseValue ∧ - post.leftValue = pre.leftValue ∧ - post.rightValue = pre.rightValue ∧ - post.owner = pre.owner - -/-- `bump()` runs `Base._bump`. -/ -def bump_spec (pre post : Storage) : Prop := - post.baseValue = pre.baseValue + 1 ∧ - post.childValue = pre.childValue ∧ - post.paused = pre.paused ∧ - post.leftValue = pre.leftValue ∧ - post.rightValue = pre.rightValue ∧ - post.owner = pre.owner - -def pauseFits (v : Storage) : Prop := - v.childValue.val + 1 ≤ Verity.Core.MAX_UINT256 - -def bumpFits (v : Storage) : Prop := - v.baseValue.val + 1 ≤ Verity.Core.MAX_UINT256 - -end Contracts.SolidityImportSmoke.Inheritance.Spec diff --git a/Contracts/SolidityImportSmoke/Inheritance/scripts/inheritance_test.py b/Contracts/SolidityImportSmoke/Inheritance/scripts/inheritance_test.py deleted file mode 100644 index 1d5e128763..0000000000 --- a/Contracts/SolidityImportSmoke/Inheritance/scripts/inheritance_test.py +++ /dev/null @@ -1,381 +0,0 @@ -#!/usr/bin/env python3 -"""Acceptance checks for the Solidity importer inheritance slice.""" - -import hashlib -import os -from pathlib import Path -import re -import shutil -import subprocess -import tempfile - -ROOT = Path(__file__).resolve().parents[4] - - -def lake_binary() -> str: - found = shutil.which("lake") - if found: - return found - version = (ROOT / "lean-toolchain").read_text().strip().split(":")[-1] - candidates = [ - Path.home() / ".elan/toolchains" / f"leanprover--lean4---{version}" / "bin/lake", - Path("/var/lib/opencode/.elan/toolchains") / f"leanprover--lean4---{version}" / "bin/lake", - ] - for candidate in candidates: - if candidate.is_file(): - return str(candidate) - raise RuntimeError("lake executable not found") - - -LAKE = lake_binary() -ENV = dict(os.environ) -SMOKE = "Contracts/SolidityImportSmoke/Inheritance" -TARGET = "SolidityImportSmokeInheritance" -NS = "Contracts.SolidityImportSmoke.Inheritance.Child" -PROOF = f"{SMOKE}/Proofs.lean" - - -def check(ok: bool, message: str) -> None: - if not ok: - raise AssertionError(message) - print("PASS " + message, flush=True) - - -def run(root: Path, args: list[str], success: bool = True, contains: str | None = None) -> str: - process = subprocess.run( - args, cwd=root, env=ENV, text=True, capture_output=True, timeout=300 - ) - output = process.stdout + process.stderr - if (process.returncode == 0) != success or (contains and contains not in output) or "PANIC" in output: - raise AssertionError(f"{args}: exit {process.returncode}\n{output}") - return output - - -def main() -> None: - importer_path = ROOT / "Contracts/VaultFromSolidity/Importer/Importer.lean" - syntax_path = ROOT / "Contracts/VaultFromSolidity/Importer/Syntax.lean" - semantics_path = ROOT / "Contracts/VaultFromSolidity/Importer/Semantics.lean" - importer_text = importer_path.read_text() - syntax_text = syntax_path.read_text() - semantics_text = semantics_path.read_text() - check("inductive FVar" in syntax_text and "inductive Args" in syntax_text, - "function table (FVar/Args) is in Syntax.lean") - check("| call " in syntax_text and "| callStmt" in syntax_text, - "internal calls are Expr.call / Stmt.callStmt") - check("FnEnv" in semantics_text and "Args.eval" in semantics_text, - "call meaning is in Semantics.lean") - check("registeredSources" in importer_text and "linearizedBaseContracts" in importer_text, - "importer accepts C3 linearization and a source manifest") - check("effectful internal call in expression position" in importer_text, - "importer rejects effectful Expr.call (legacy codegen order)") - check("super dispatch family mismatch" in importer_text and - "super dispatch disagrees with AST" not in importer_text, - "super follows target C3 rather than defining-contract AST id") - - with tempfile.TemporaryDirectory(prefix="verity-inh-check-", dir=ROOT.parent) as directory: - root = Path(directory) - for name in ("Verity", "Compiler", "Contracts", "scripts"): - shutil.copytree(ROOT / name, root / name) - for name in ("lakefile.lean", "lake-manifest.json", "lean-toolchain"): - shutil.copy2(ROOT / name, root / name) - shutil.copytree(ROOT / ".lake/build", root / ".lake/build") - (root / ".lake/solidity-import").mkdir(parents=True) - shutil.copy2(ROOT / ".lake/solidity-import/solc", root / ".lake/solidity-import/solc") - (root / ".lake/packages").symlink_to(ROOT / ".lake/packages", target_is_directory=True) - - source = root / SMOKE / "Inheritance.sol" - original = source.read_bytes() - source_stamp = source.stat() - importer = root / "Contracts/VaultFromSolidity/Importer/Importer.lean" - importer_original = importer.read_bytes() - importer_stamp = importer.stat() - proof = root / PROOF - proof_text = proof.read_text() - theorem_names = re.findall(r"^theorem\s+(\w+)", proof_text, re.M) - - def edit_source(data: bytes) -> None: - source.write_bytes(data) - os.utime(source, ns=(source_stamp.st_atime_ns, source_stamp.st_mtime_ns)) - - def build(success: bool = True, contains: str | None = None) -> str: - return run(root, [LAKE, "build", TARGET], success, contains) - - def artifacts() -> dict[str, tuple[int, str]]: - paths = list((root / ".lake/build/lib/lean/Contracts/SolidityImportSmoke/Inheritance").rglob("*.olean")) - return { - str(path.relative_to(root)): ( - path.stat().st_mtime_ns, - hashlib.sha256(path.read_bytes()).hexdigest(), - ) - for path in paths - } - - def source_digest() -> str: - digest_probe = root / ".lake/solidity-import/InhDigestProbe.lean" - try: - digest_probe.write_text( - f"import {SMOKE.replace('/', '.')}.Inheritance\n" - f"#eval {NS}.sourceDigest\n" - ) - output = run(root, [LAKE, "env", "lean", str(digest_probe)]) - match = re.search(r'"([0-9a-f]{64})"', output) - if match is None: - raise AssertionError("sourceDigest is not an auditable SHA-256 value") - return match.group(1) - finally: - digest_probe.unlink(missing_ok=True) - - def slot_numbers() -> str: - slot_probe = root / ".lake/solidity-import/InhSlotProbe.lean" - try: - slot_probe.write_text( - f"import {SMOKE.replace('/', '.')}.Inheritance\n" - f"#eval [{NS}.baseValueSlot.slot, {NS}.leftValueSlot.slot, " - f"{NS}.rightValueSlot.slot, {NS}.pausedSlot.slot, " - f"{NS}.ownerSlot.slot, {NS}.childValueSlot.slot]\n" - ) - found = re.findall(r"\[\d+(?:, \d+)*\]", run(root, [LAKE, "env", "lean", str(slot_probe)])) - if not found: - raise AssertionError("slot probe produced no list") - return found[0] - finally: - slot_probe.unlink(missing_ok=True) - - build() - check(True, "baseline lake build SolidityImportSmokeInheritance") - - audit_file = root / ".lake/solidity-import/InhAxiomAudit.lean" - try: - audit_file.write_text( - f"import {SMOKE.replace('/', '.')}.Proofs\n" - + "\n".join( - f"#print axioms {SMOKE.replace('/', '.')}.Proofs.{name}" - for name in theorem_names - ) - + "\n" - ) - audit = run(root, [LAKE, "env", "lean", str(audit_file)]) - finally: - audit_file.unlink(missing_ok=True) - entries = re.findall( - r"'Contracts.SolidityImportSmoke.Inheritance.Proofs.(\w+)' depends on axioms: \[([^\]]*)\]", - audit, - ) - check(set(theorem_names) == {name for name, _ in entries}, - "every inheritance theorem appears in #print axioms output") - axioms = {a.strip() for _, values in entries for a in values.split(",") if a.strip()} - check(axioms <= {"propext", "Quot.sound"}, - "no project axioms or sorryAx: " + ", ".join(sorted(axioms))) - - print_probe = root / ".lake/solidity-import/InhPrintProbe.lean" - try: - print_probe.write_text( - f"import {SMOKE.replace('/', '.')}.Inheritance\n" - f"#print {NS}.go\n" - f"#print {NS}.Child__pause\n" - ) - printed = run(root, [LAKE, "env", "lean", str(print_probe)]) - check("Child__pause" in printed and "callStmt" in printed, - "#print Child.go shows a call to Child__pause") - check("PausableLike__pause" in printed, - "#print Child__pause shows the super hop to PausableLike__pause") - finally: - print_probe.unlink(missing_ok=True) - - def import_only(success: bool = True, contains: str | None = None) -> str: - # Rebuild the importer module without Proofs.lean: diamond internals - # enlarge FnEnv enough that the smoke proofs' `simp` budget fails, - # but solc C3 super still has to elaborate. - return run( - root, - [LAKE, "build", "Contracts.SolidityImportSmoke.Inheritance.Inheritance"], - success, - contains, - ) - - def print_names(*names: str) -> str: - probe = root / ".lake/solidity-import/InhPrintNames.lean" - try: - probe.write_text( - f"import {SMOKE.replace('/', '.')}.Inheritance\n" - + "".join(f"#print {NS}.{name}\n" for name in names) - ) - return run(root, [LAKE, "env", "lean", str(probe)]) - finally: - probe.unlink(missing_ok=True) - - slots = slot_numbers() - check(slots == "[0, 1, 2, 3, 4, 5]", - "inherited slots follow solc layout [base, left, right, paused, owner, child]") - - swapped = original.replace(b"is Left, Right, PausableLike, OwnableLike", - b"is Right, Left, PausableLike, OwnableLike") - check(original != swapped, "is-order mutation has a source target") - edit_source(swapped) - build() - check(slot_numbers() == "[0, 2, 1, 3, 4, 5]", - "swapping is-order changes C3 and inherited slot numbers") - edit_source(original) - build() - - diamond = original.replace( - b" function _bump() internal virtual {\n baseValue += 1;\n }\n", - b" function _bump() internal virtual {\n baseValue += 1;\n }\n" - b" function _trace() internal virtual {}\n", - ).replace( - b"abstract contract Left is Base {\n uint256 public leftValue;\n}\n", - b"abstract contract Left is Base {\n uint256 public leftValue;\n" - b" function _trace() internal virtual override { super._trace(); }\n}\n", - ).replace( - b"abstract contract Right is Base {\n uint256 public rightValue;\n}\n", - b"abstract contract Right is Base {\n uint256 public rightValue;\n" - b" function _trace() internal virtual override {}\n}\n", - ).replace( - b" function go() external {\n _pause();\n }\n", - b" function _trace() internal override(Left, Right) { super._trace(); }\n" - b" function go() external {\n _pause();\n }\n", - ) - check(original != diamond, "diamond virtual mutation has a source target") - edit_source(diamond) - import_only() - diamond_printed = print_names("Child__trace", "Left__trace") - check("callStmt" in diamond_printed, - "diamond super on default is-order imports as callStmt") - # `is Right, Left` makes Left more derived than Right. Left._trace's AST - # super target is Base._trace; target C3 super is Right._trace. - diamond_swapped = diamond.replace( - b"is Left, Right, PausableLike, OwnableLike", - b"is Right, Left, PausableLike, OwnableLike") - edit_source(diamond_swapped) - swapped_import = import_only() - check("super dispatch disagrees with AST" not in swapped_import, - "diamond super follows target C3, not defining-contract AST id") - check("callStmt" in print_names("Left__trace"), - "#print Left__trace after diamond is-order swap still has super callStmt") - edit_source(original) - build() - - sum_src = original.replace( - b" function _add(uint256 a, uint256 b) internal pure returns (uint256) {\n" - b" return a + b;\n }\n", - b" function _add(uint256 a, uint256 b) internal pure returns (uint256) {\n" - b" return a + b;\n }\n" - b" function _sum() internal pure returns (uint256) {\n" - b" return _add(1, 2);\n }\n", - ) - check(original != sum_src, "pure Expr.call mutation has a source target") - edit_source(sum_src) - import_only() - check("Child__add" in print_names("Child__sum"), - "#print Child__sum shows Expr.call of pure Child__add") - edit_source(original) - build() - - def broken_theorems(output: str) -> set[str]: - error_lines = [int(value) for value in re.findall(rf"{PROOF}:(\d+):", output)] - starts = [ - (match.group(1), line_no) - for line_no, line in enumerate(proof_text.splitlines(), 1) - if (match := re.match(r"theorem\s+([A-Za-z0-9_']+)", line.strip())) - ] - ranges = { - name: (start, starts[index + 1][1] - 1 if index + 1 < len(starts) - else len(proof_text.splitlines())) - for index, (name, start) in enumerate(starts) - } - return {name for name, (start, end) in ranges.items() - if any(start <= line <= end for line in error_lines)} - - override = b" childValue += 1;" - check(original.count(override) == 1, "override body has one source target") - edit_source(original.replace(override, b" childValue = 0;")) - output = build(False, "Contracts.SolidityImportSmoke.Inheritance.Proofs") - broken = broken_theorems(output) - check(bool({"dispatch_is_child", "pause_success_spec", "go_success_spec"} & broken), - "changing the override body breaks the dispatch observation") - edit_source(original) - build() - - pause_fn = b" function pause() external {\n _pause();\n }\n" - extra = b" function ping() external {\n paused = paused;\n }\n" + pause_fn - check(original.count(pause_fn) == 1, "base public function has one source target") - edit_source(original.replace(pause_fn, extra)) - output = build(False, "Contracts.SolidityImportSmoke.Inheritance.Proofs") - check("paused_invariant" in broken_theorems(output), - "adding a public function to a base breaks paused_invariant") - edit_source(original) - build() - - before = artifacts() - digest_before = source_digest() - importer.write_bytes(importer_original + b"\n-- inheritance identity probe\n") - os.utime(importer, ns=(importer_stamp.st_atime_ns, importer_stamp.st_mtime_ns)) - build() - check(before != artifacts(), "Lean importer content change invalidates inheritance artifacts") - check(digest_before != source_digest(), "Lean importer content changes sourceDigest") - importer.write_bytes(importer_original) - build() - - semantics = root / "Contracts/VaultFromSolidity/Importer/Semantics.lean" - semantics_original = semantics.read_bytes() - semantics_stamp = semantics.stat() - before = artifacts() - digest_before = source_digest() - semantics.write_bytes(semantics_original + b"\n-- inheritance semantics identity probe\n") - os.utime(semantics, ns=(semantics_stamp.st_atime_ns, semantics_stamp.st_mtime_ns)) - build() - check(before != artifacts(), "Lean semantics content change invalidates inheritance artifacts") - check(digest_before != source_digest(), "Lean semantics content changes sourceDigest") - semantics.write_bytes(semantics_original) - build() - - for name, old, new, diagnostic in ( - ("direct recursion", - b" super._pause();\n childValue += 1;", - b" _pause();\n childValue += 1;", - "recursion"), - ("unimplemented virtual", - b" function _bump() internal virtual {\n baseValue += 1;\n }", - b" function _bump() internal virtual;", - "abstract"), - ("opaque field touch", - b" function go() external {\n _pause();\n }", - b" function go() external {\n unusedGap = unusedGap;\n _pause();\n }", - "opaque field"), - ("function overloading", - b" function bump() external {\n _bump();\n }", - b" function bump() external {\n _bump();\n }\n" - b" function bump(uint256 x) external {\n baseValue = x;\n }", - "overloading"), - ("effectful add of internal call", - b" function bump() external {\n _bump();\n }", - b" function _inc() internal returns (uint256) {\n" - b" childValue += 1;\n return childValue;\n }\n" - b" function bump() external {\n" - b" childValue = childValue + _inc();\n }", - "effectful internal call in expression position"), - ("effectful compound assignment of internal call", - b" function bump() external {\n _bump();\n }", - b" function _inc() internal returns (uint256) {\n" - b" childValue += 1;\n return childValue;\n }\n" - b" function bump() external {\n" - b" childValue += _inc();\n }", - "effectful internal call in expression position"), - ): - check(original.count(old) == 1, name + " mutation has one source target") - edit_source(original.replace(old, new)) - output = build(False, diagnostic) - check(re.search(r"Contracts/SolidityImportSmoke/Inheritance/Inheritance.sol:\d+:\d+:", output) - is not None, - name + " rejected with source position") - edit_source(original) - build() - - check(source.read_bytes() == original and importer.read_bytes() == importer_original, - "temporary mutations restored; final baseline passes") - print(f"PASS all inheritance-slice acceptance checks ({len(theorem_names)} audited theorems)", - flush=True) - - -if __name__ == "__main__": - main() diff --git a/Contracts/SolidityImportSmoke/Modifiers/Modifiers.lean b/Contracts/SolidityImportSmoke/Modifiers/Modifiers.lean deleted file mode 100644 index 0738eb2da4..0000000000 --- a/Contracts/SolidityImportSmoke/Modifiers/Modifiers.lean +++ /dev/null @@ -1,7 +0,0 @@ -import Contracts.VaultFromSolidity.Importer.Importer - -namespace Contracts.SolidityImportSmoke.Modifiers - -solidity_contract Child from "Modifiers.sol" - -end Contracts.SolidityImportSmoke.Modifiers diff --git a/Contracts/SolidityImportSmoke/Modifiers/Modifiers.sol b/Contracts/SolidityImportSmoke/Modifiers/Modifiers.sol deleted file mode 100644 index 4f66957a90..0000000000 --- a/Contracts/SolidityImportSmoke/Modifiers/Modifiers.sol +++ /dev/null @@ -1,93 +0,0 @@ -// SPDX-License-Identifier: MIT -pragma solidity ^0.8.33; - -error NotOwner(); -error EnforcedPause(); -error ReentrancyGuardReentrantCall(); - -abstract contract OwnableLike { - address public owner; - - modifier onlyOwner() { - if (msg.sender != owner) revert NotOwner(); - _; - } -} - -abstract contract PausableLike { - uint256 public paused; - - modifier whenNotPaused() { - if (0 < paused) revert EnforcedPause(); - _; - } -} - -abstract contract ReentrancyGuardLike { - uint256 public status; - - modifier nonReentrant() { - if (1 < status) revert ReentrancyGuardReentrantCall(); - status = 2; - _; - status = 1; - } - - modifier restore() { - uint256 old = status; - _; - status = old; - } -} - -abstract contract HelperLike { - uint256 public helperValue; - - modifier bumpHelper() { - bump(); - _; - } - - function bump() private { - helperValue = 1; - } -} - -contract Child is OwnableLike, PausableLike, ReentrancyGuardLike, HelperLike { - uint256 public value; - - function setOwner(address next) external { - owner = next; - } - - function arm() external { - status = 1; - } - - function guarded(uint256 amount) external onlyOwner whenNotPaused { - value = amount; - } - - function enter(uint256 amount) external nonReentrant returns (uint256) { - value = amount; - return amount; - } - - function early(uint256 amount) external nonReentrant returns (uint256) { - value = amount; - return amount; - } - - function poison() private { - helperValue = 2; - } - - function tagged() external bumpHelper { - value = 0; - } - - function snapshot(uint256 amount) external restore returns (uint256) { - status = amount; - return amount; - } -} diff --git a/Contracts/SolidityImportSmoke/Modifiers/Proofs.lean b/Contracts/SolidityImportSmoke/Modifiers/Proofs.lean deleted file mode 100644 index 62c850e97a..0000000000 --- a/Contracts/SolidityImportSmoke/Modifiers/Proofs.lean +++ /dev/null @@ -1,75 +0,0 @@ -import Contracts.SolidityImportSmoke.Modifiers.Spec -import Verity.Proofs.Stdlib.SolidityImport - -namespace Contracts.SolidityImportSmoke.Modifiers.Proofs -open Verity -open Verity.Proofs.Stdlib.SolidityImport -open Contracts.SolidityImportSmoke.Modifiers.Child -open Spec - -theorem status_restored (s post : ContractState) (amount : Uint256) - (h : (early amount).run s = .success amount post) : - early_spec amount (view s) (view post) := by - unfold early_spec - by_cases h0 : s.msgValue = 0 - · by_cases hst : 1 < (view s).status.val - · solidity_simp - · solidity_simp - subst h - solidity_simp - · solidity_simp - -theorem early_meets_spec (s : ContractState) (amount : Uint256) - (h0 : s.msgValue = 0) (harmed : (view s).status = 1) : - ∃ post, (early amount).run s = ContractResult.success amount post ∧ - early_spec amount (view s) (view post) := by - unfold early_spec - solidity_simp - -theorem guarded_success_spec (s post : ContractState) (amount : Uint256) - (h : (guarded amount).run s = .success () post) : - guarded_spec amount (view s) (view post) := by - unfold guarded_spec - by_cases h0 : s.msgValue = 0 - · by_cases ho : s.sender = (view s).owner - · by_cases hp : (view s).paused.val = 0 - · solidity_simp - subst h - solidity_simp - · solidity_simp - · by_cases hp : (view s).paused.val = 0 - · solidity_simp - · solidity_simp - · solidity_simp - -theorem guarded_reverts_not_owner_before_pause (s : ContractState) (amount : Uint256) - (h0 : s.msgValue = 0) - (howner : s.sender ≠ (view s).owner) - (hpaused : 0 < (view s).paused.val) : - (guarded amount).run s = ContractResult.revert "NotOwner()" s := by - by_cases ho : s.sender = (view s).owner - · exact (howner ho).elim - · have := hpaused - solidity_simp - -theorem tagged_uses_base_helper (s post : ContractState) - (h : tagged.run s = .success () post) : - tagged_spec (view s) (view post) := by - unfold tagged_spec - by_cases h0 : s.msgValue = 0 - · solidity_simp - subst h - solidity_simp - · solidity_simp - -theorem snapshot_restores_status (s post : ContractState) (amount : Uint256) - (h : (snapshot amount).run s = .success amount post) : - snapshot_spec amount (view s) (view post) := by - unfold snapshot_spec - by_cases h0 : s.msgValue = 0 - · solidity_simp - subst h - solidity_simp - · solidity_simp - -end Contracts.SolidityImportSmoke.Modifiers.Proofs diff --git a/Contracts/SolidityImportSmoke/Modifiers/Spec.lean b/Contracts/SolidityImportSmoke/Modifiers/Spec.lean deleted file mode 100644 index 4b7d370096..0000000000 --- a/Contracts/SolidityImportSmoke/Modifiers/Spec.lean +++ /dev/null @@ -1,36 +0,0 @@ -import Verity.EVM.Uint256 -import Contracts.SolidityImportSmoke.Modifiers.Modifiers - -namespace Contracts.SolidityImportSmoke.Modifiers.Spec - -open Verity -open Verity.EVM.Uint256 -open Contracts.SolidityImportSmoke.Modifiers.Child - -def early_spec (amount : Uint256) (pre post : Storage) : Prop := - post.status = 1 ∧ - post.value = amount ∧ - post.owner = pre.owner ∧ - post.paused = pre.paused - -def guarded_spec (amount : Uint256) (pre post : Storage) : Prop := - post.value = amount ∧ - post.status = pre.status ∧ - post.owner = pre.owner ∧ - post.paused = pre.paused - -def tagged_spec (pre post : Storage) : Prop := - post.helperValue = 1 ∧ - post.value = 0 ∧ - post.status = pre.status ∧ - post.owner = pre.owner ∧ - post.paused = pre.paused - -def snapshot_spec (_amount : Uint256) (pre post : Storage) : Prop := - post.status = pre.status ∧ - post.value = pre.value ∧ - post.owner = pre.owner ∧ - post.paused = pre.paused ∧ - post.helperValue = pre.helperValue - -end Contracts.SolidityImportSmoke.Modifiers.Spec diff --git a/Contracts/SolidityImportSmoke/Modifiers/scripts/modifiers_test.py b/Contracts/SolidityImportSmoke/Modifiers/scripts/modifiers_test.py deleted file mode 100755 index 51f058736a..0000000000 --- a/Contracts/SolidityImportSmoke/Modifiers/scripts/modifiers_test.py +++ /dev/null @@ -1,268 +0,0 @@ -#!/usr/bin/env python3 -"""Acceptance checks for the Solidity importer modifiers slice.""" - -import hashlib -import os -from pathlib import Path -import re -import shutil -import subprocess -import tempfile - -ROOT = Path(__file__).resolve().parents[4] - - -def lake_binary() -> str: - version = (ROOT / "lean-toolchain").read_text().strip().split(":")[-1] - candidates = [ - Path.home() / ".elan/toolchains" / f"leanprover--lean4---{version}" / "bin/lake", - Path("/var/lib/opencode/.elan/toolchains") / f"leanprover--lean4---{version}" / "bin/lake", - ] - for candidate in candidates: - if candidate.is_file(): - return str(candidate) - found = shutil.which("lake") - if found: - return found - raise RuntimeError("lake executable not found") - - -LAKE = lake_binary() -ENV = dict(os.environ) -SMOKE = "Contracts/SolidityImportSmoke/Modifiers" -TARGET = "SolidityImportSmokeModifiers" -NS = "Contracts.SolidityImportSmoke.Modifiers.Child" -PROOF = f"{SMOKE}/Proofs.lean" - - -def check(ok: bool, message: str) -> None: - if not ok: - raise AssertionError(message) - print("PASS " + message, flush=True) - - -def run(root: Path, args: list[str], success: bool = True, contains: str | None = None) -> str: - process = subprocess.run( - args, cwd=root, env=ENV, text=True, capture_output=True, timeout=300 - ) - output = process.stdout + process.stderr - if (process.returncode == 0) != success or (contains and contains not in output) or "PANIC" in output: - raise AssertionError(f"{args}: exit {process.returncode}\n{output}") - return output - - -def main() -> None: - importer_path = ROOT / "Contracts/VaultFromSolidity/Importer/Importer.lean" - syntax_path = ROOT / "Contracts/VaultFromSolidity/Importer/Syntax.lean" - semantics_path = ROOT / "Contracts/VaultFromSolidity/Importer/Semantics.lean" - importer_text = importer_path.read_text() - syntax_text = syntax_path.read_text() - semantics_text = semantics_path.read_text() - check("| seq " in syntax_text and "| block " in syntax_text, - "modifier inlining uses Stmt.seq / Stmt.block") - check("Semantics.lean never sees `_`" in semantics_text or - "this file never sees `_`" in semantics_text, - "modifier placeholder is inlined at parse time") - check("wrapModifiers" in importer_text and "modifier with two placeholders" in importer_text, - "importer inlines modifiers and rejects two placeholders") - check("modifier arguments" in importer_text, - "importer rejects parameterized modifiers") - check("static.visibility == \"private\"" in importer_text and "!static.virtual" in importer_text, - "identifier calls keep private/non-virtual static binding") - check("wrapModifierParts" in importer_text and "inner.shift .uint 0" in importer_text, - "prelude locals are bound over the inlined body and postlude") - - with tempfile.TemporaryDirectory(prefix="verity-mod-check-", dir=ROOT.parent) as directory: - root = Path(directory) - for name in ("Verity", "Compiler", "Contracts", "scripts"): - shutil.copytree(ROOT / name, root / name) - for name in ("lakefile.lean", "lake-manifest.json", "lean-toolchain"): - shutil.copy2(ROOT / name, root / name) - shutil.copytree(ROOT / ".lake/build", root / ".lake/build") - (root / ".lake/solidity-import").mkdir(parents=True) - shutil.copy2(ROOT / ".lake/solidity-import/solc", root / ".lake/solidity-import/solc") - (root / ".lake/packages").symlink_to(ROOT / ".lake/packages", target_is_directory=True) - - source = root / SMOKE / "Modifiers.sol" - original = source.read_bytes() - source_stamp = source.stat() - importer = root / "Contracts/VaultFromSolidity/Importer/Importer.lean" - importer_original = importer.read_bytes() - importer_stamp = importer.stat() - proof = root / PROOF - proof_text = proof.read_text() - theorem_names = re.findall(r"^theorem\s+(\w+)", proof_text, re.M) - - def edit_source(data: bytes) -> None: - source.write_bytes(data) - os.utime(source, ns=(source_stamp.st_atime_ns, source_stamp.st_mtime_ns)) - - def build(success: bool = True, contains: str | None = None) -> str: - return run(root, [LAKE, "build", TARGET], success, contains) - - def artifacts() -> dict[str, tuple[int, str]]: - paths = list((root / ".lake/build/lib/lean/Contracts/SolidityImportSmoke/Modifiers").rglob("*.olean")) - return { - str(path.relative_to(root)): ( - path.stat().st_mtime_ns, - hashlib.sha256(path.read_bytes()).hexdigest(), - ) - for path in paths - } - - def print_names(*names: str) -> str: - probe = root / ".lake/solidity-import/ModPrint.lean" - try: - probe.write_text( - f"import {SMOKE.replace('/', '.')}.Modifiers\n" - + "".join(f"#print {NS}.{name}\n" for name in names) - ) - return run(root, [LAKE, "env", "lean", str(probe)]) - finally: - probe.unlink(missing_ok=True) - - build() - check(True, "baseline lake build SolidityImportSmokeModifiers") - - audit_file = root / ".lake/solidity-import/ModAxiomAudit.lean" - try: - audit_file.write_text( - f"import {SMOKE.replace('/', '.')}.Proofs\n" - + "\n".join( - f"#print axioms {SMOKE.replace('/', '.')}.Proofs.{name}" - for name in theorem_names - ) - + "\n" - ) - audit = run(root, [LAKE, "env", "lean", str(audit_file)]) - finally: - audit_file.unlink(missing_ok=True) - entries = re.findall( - r"'Contracts.SolidityImportSmoke.Modifiers.Proofs.(\w+)' depends on axioms: \[([^\]]*)\]", - audit, - ) - check(set(theorem_names) == {name for name, _ in entries}, - "every modifier theorem appears in #print axioms output") - axioms = {a.strip() for _, values in entries for a in values.split(",") if a.strip()} - check(axioms <= {"propext", "Quot.sound"}, - "no project axioms or sorryAx: " + ", ".join(sorted(axioms))) - - printed_early = print_names("early") - check(".seq" in printed_early and ".block" in printed_early, - "#print Child.early inlines nonReentrant as seq/block") - printed_guarded = print_names("guarded") - check(printed_guarded.find("NotOwner") < printed_guarded.find("EnforcedPause"), - "onlyOwner then whenNotPaused is declaration order") - printed_tagged = print_names("tagged") - check("HelperLike_bump" in printed_tagged and "Child_bump" not in printed_tagged, - "inherited modifier keeps the declaring contract's private helper") - printed_snapshot = print_names("snapshot") - check(".local_" in printed_snapshot and ".block" in printed_snapshot, - "#print Child.snapshot carries restore prelude local into the postlude") - - def broken_theorems(output: str) -> set[str]: - error_lines = [int(value) for value in re.findall(rf"{PROOF}:(\d+):", output)] - starts = [ - (match.group(1), line_no) - for line_no, line in enumerate(proof_text.splitlines(), 1) - if (match := re.match(r"theorem\s+([A-Za-z0-9_']+)", line.strip())) - ] - ranges = { - name: (start, starts[index + 1][1] - 1 if index + 1 < len(starts) - else len(proof_text.splitlines())) - for index, (name, start) in enumerate(starts) - } - return {name for name, (start, end) in ranges.items() - if any(start <= line <= end for line in error_lines)} - - postlude = b" status = 2;\n _;\n status = 1;" - dropped = b" status = 2;\n _;" - check(original.count(postlude) == 1, "nonReentrant postlude has one source target") - edit_source(original.replace(postlude, dropped)) - output = build(False, "Contracts.SolidityImportSmoke.Modifiers.Proofs") - check(bool({"status_restored", "early_meets_spec"} & broken_theorems(output)), - "dropping the nonReentrant postlude breaks status_restored") - edit_source(original) - build() - - swapped = original.replace(b"onlyOwner whenNotPaused", b"whenNotPaused onlyOwner") - check(original != swapped, "modifier-order mutation has a source target") - edit_source(swapped) - output = build(False, "Contracts.SolidityImportSmoke.Modifiers.Proofs") - check("guarded_reverts_not_owner_before_pause" in broken_theorems(output), - "swapping onlyOwner/whenNotPaused breaks guarded_reverts_not_owner_before_pause") - edit_source(original) - build() - - before = artifacts() - importer.write_bytes(importer_original + b"\n-- modifiers identity probe\n") - os.utime(importer, ns=(importer_stamp.st_atime_ns, importer_stamp.st_mtime_ns)) - build() - check(before != artifacts(), "Lean importer content change invalidates modifier artifacts") - importer.write_bytes(importer_original) - build() - - two_ph_old = b" if (msg.sender != owner) revert NotOwner();\n _;" - two_ph_new = b" if (msg.sender != owner) revert NotOwner();\n _;\n _;" - check(original.count(two_ph_old) == 1, "two placeholders mutation has one source target") - edit_source(original.replace(two_ph_old, two_ph_new)) - output = build(False, "modifier with two placeholders") - check(re.search(r"Contracts/SolidityImportSmoke/Modifiers/Modifiers.sol:\d+:\d+:", output) - is not None, - "two placeholders rejected with source position") - edit_source(original) - - args_src = original.replace( - b" modifier onlyOwner() {\n if (msg.sender != owner) revert NotOwner();\n _;\n }", - b" modifier onlyOwner(uint256 unused) {\n unused;\n if (msg.sender != owner) revert NotOwner();\n _;\n }", - ).replace( - b" function guarded(uint256 amount) external onlyOwner whenNotPaused {", - b" function guarded(uint256 amount) external onlyOwner(0) whenNotPaused {", - ) - check(args_src != original, "modifier arguments mutation has a source target") - edit_source(args_src) - output = build(False, "modifier arguments") - check(re.search(r"Contracts/SolidityImportSmoke/Modifiers/Modifiers.sol:\d+:\d+:", output) - is not None, - "modifier arguments rejected with source position") - edit_source(original) - build() - - restore_post = b" uint256 old = status;\n _;\n status = old;" - restore_dropped = b" uint256 old = status;\n _;" - check(original.count(restore_post) == 1, "restore postlude has one source target") - edit_source(original.replace(restore_post, restore_dropped)) - output = build(False, "Contracts.SolidityImportSmoke.Modifiers.Proofs") - check(bool({"snapshot_restores_status"} & broken_theorems(output)), - "dropping the restore postlude breaks snapshot_restores_status") - edit_source(original) - build() - - base_helper = b" function bump() private {\n helperValue = 1;\n }" - base_helper_mut = b" function bump() private {\n helperValue = 9;\n }" - check(original.count(base_helper) == 1, "base private helper has one source target") - edit_source(original.replace(base_helper, base_helper_mut)) - output = build(False, "Contracts.SolidityImportSmoke.Modifiers.Proofs") - check(bool({"tagged_uses_base_helper"} & broken_theorems(output)), - "mutating the declaring contract's private helper breaks tagged_uses_base_helper") - edit_source(original) - build() - poison = b" function poison() private {\n helperValue = 2;\n }" - poison_mut = b" function poison() private {\n helperValue = 9;\n }" - check(original.count(poison) == 1, "unrelated derived private helper has one source target") - edit_source(original.replace(poison, poison_mut)) - build() - mutated_tagged = print_names("tagged") - check("HelperLike_bump" in mutated_tagged and "Child_bump" not in mutated_tagged, - "mutating an unrelated derived private helper does not rebind the inherited modifier") - edit_source(original) - build() - - check(source.read_bytes() == original and importer.read_bytes() == importer_original, - "temporary mutations restored; final baseline passes") - print(f"PASS all modifier-slice acceptance checks ({len(theorem_names)} audited theorems)", - flush=True) - - -if __name__ == "__main__": - main() diff --git a/Contracts/SolidityImportSmoke/Structs/Proofs.lean b/Contracts/SolidityImportSmoke/Structs/Proofs.lean deleted file mode 100644 index 0a77c561df..0000000000 --- a/Contracts/SolidityImportSmoke/Structs/Proofs.lean +++ /dev/null @@ -1,39 +0,0 @@ -import Contracts.SolidityImportSmoke.Structs.Spec -import Verity.Proofs.Stdlib.SolidityImport - -namespace Contracts.SolidityImportSmoke.Structs.Proofs -open Verity -open Verity.Proofs.Stdlib.SolidityImport -open Contracts.SolidityImportSmoke.Structs.Store -open Spec - -theorem set_then_get (s post : ContractState) (amount : Uint256) (who : Address) - (h : (set (amount, who)).run s = .success () post) : - set_spec amount who (view s) (view post) := by - unfold set_spec - cases hbeq : Nat.beq s.msgValue.val 0 - · solidity_simp - · solidity_simp - subst h - solidity_simp - -theorem other_member_unchanged_thm (s post : ContractState) (amount : Uint256) (who : Address) - (h : (set (amount, who)).run s = .success () post) : - other_member_unchanged (amount, who) (view s) (view post) := - (set_then_get s post amount who h).2.2 - -theorem get_meets_spec (s : ContractState) (h0 : s.msgValue = 0) : - ∃ post, get.run s = ContractResult.success ((view s).data_amount, (view s).data_who) post := by - solidity_simp - -theorem make_encodes (s : ContractState) (amount : Uint256) (who : Address) - (h0 : s.msgValue = 0) : - ∃ post, (make amount who).run s = ContractResult.success (amount, who) post := by - solidity_simp - -theorem makeRev_encodes (s : ContractState) (amount : Uint256) (who : Address) - (h0 : s.msgValue = 0) : - ∃ post, (makeRev amount who).run s = ContractResult.success (amount, who) post := by - solidity_simp - -end Contracts.SolidityImportSmoke.Structs.Proofs diff --git a/Contracts/SolidityImportSmoke/Structs/Spec.lean b/Contracts/SolidityImportSmoke/Structs/Spec.lean deleted file mode 100644 index 47883c4fc3..0000000000 --- a/Contracts/SolidityImportSmoke/Structs/Spec.lean +++ /dev/null @@ -1,17 +0,0 @@ -import Verity.EVM.Uint256 -import Contracts.SolidityImportSmoke.Structs.Structs - -namespace Contracts.SolidityImportSmoke.Structs.Spec - -open Verity -open Contracts.SolidityImportSmoke.Structs.Store - -def set_spec (amount : Uint256) (who : Address) (pre post : Storage) : Prop := - post.data_amount = amount ∧ - post.data_who = who ∧ - post.other = pre.other - -def other_member_unchanged (_a : Uint256 × Address) (pre post : Storage) : Prop := - post.other = pre.other - -end Contracts.SolidityImportSmoke.Structs.Spec diff --git a/Contracts/SolidityImportSmoke/Structs/Structs.lean b/Contracts/SolidityImportSmoke/Structs/Structs.lean deleted file mode 100644 index ba4de8e030..0000000000 --- a/Contracts/SolidityImportSmoke/Structs/Structs.lean +++ /dev/null @@ -1,7 +0,0 @@ -import Contracts.VaultFromSolidity.Importer.Importer - -namespace Contracts.SolidityImportSmoke.Structs - -solidity_contract Store from "Structs.sol" - -end Contracts.SolidityImportSmoke.Structs diff --git a/Contracts/SolidityImportSmoke/Structs/Structs.sol b/Contracts/SolidityImportSmoke/Structs/Structs.sol deleted file mode 100644 index 99a6ae6c55..0000000000 --- a/Contracts/SolidityImportSmoke/Structs/Structs.sol +++ /dev/null @@ -1,32 +0,0 @@ -// SPDX-License-Identifier: MIT -pragma solidity ^0.8.33; - -struct Acc { - uint256 amount; - address who; -} - -contract Store { - Acc public data; - uint256 public other; - - function set(Acc memory a) external { - data = a; - } - - function get() external view returns (Acc memory) { - return data; - } - - function setOther(uint256 n) external { - other = n; - } - - function make(uint256 amount, address who) external pure returns (Acc memory) { - return Acc({amount: amount, who: who}); - } - - function makeRev(uint256 amount, address who) external pure returns (Acc memory) { - return Acc({who: who, amount: amount}); - } -} diff --git a/Contracts/SolidityImportSmoke/Structs/scripts/structs_test.py b/Contracts/SolidityImportSmoke/Structs/scripts/structs_test.py deleted file mode 100755 index a11316103c..0000000000 --- a/Contracts/SolidityImportSmoke/Structs/scripts/structs_test.py +++ /dev/null @@ -1,293 +0,0 @@ -#!/usr/bin/env python3 -"""Acceptance checks for the Solidity importer structs slice.""" - -import hashlib -import os -from pathlib import Path -import re -import shutil -import subprocess -import tempfile - -ROOT = Path(__file__).resolve().parents[4] - - -def lake_binary() -> str: - version = (ROOT / "lean-toolchain").read_text().strip().split(":")[-1] - candidates = [ - Path.home() / ".elan/toolchains" / f"leanprover--lean4---{version}" / "bin/lake", - Path("/var/lib/opencode/.elan/toolchains") / f"leanprover--lean4---{version}" / "bin/lake", - ] - for candidate in candidates: - if candidate.is_file(): - return str(candidate) - found = shutil.which("lake") - if found: - return found - raise RuntimeError("lake executable not found") - - -LAKE = lake_binary() -ENV = dict(os.environ) -SMOKE = "Contracts/SolidityImportSmoke/Structs" -TARGET = "SolidityImportSmokeStructs" -NS = "Contracts.SolidityImportSmoke.Structs.Store" -PROOF = f"{SMOKE}/Proofs.lean" - - -def check(ok: bool, message: str) -> None: - if not ok: - raise AssertionError(message) - print("PASS " + message, flush=True) - - -def run(root: Path, args: list[str], success: bool = True, contains: str | None = None) -> str: - process = subprocess.run( - args, cwd=root, env=ENV, text=True, capture_output=True, timeout=300 - ) - output = process.stdout + process.stderr - if (process.returncode == 0) != success or (contains and contains not in output) or "PANIC" in output: - raise AssertionError(f"{args}: exit {process.returncode}\n{output}") - return output - - -def main() -> None: - importer_path = ROOT / "Contracts/VaultFromSolidity/Importer/Importer.lean" - syntax_path = ROOT / "Contracts/VaultFromSolidity/Importer/Syntax.lean" - importer_text = importer_path.read_text() - syntax_text = syntax_path.read_text() - check("| pair " in syntax_text and "| fst " in syntax_text and "| snd " in syntax_text, - "user structs are Expr.pair / fst / snd") - check("StructDefinition" in importer_text and "UserDefinedTypeName" in importer_text, - "importer accepts struct AST nodes") - check("structConstructorCall" in importer_text, - "importer encodes struct constructors") - check("| pairRev " in syntax_text and ".pairRev " in importer_text, - "named Acc({who, amount}) uses pairRev after source-order eval") - check(".pairRev a b =>" in (ROOT / "Contracts/VaultFromSolidity/Importer/Semantics.lean").read_text(), - "pairRev evaluates who then amount and packs uint-then-addr") - check("struct with a dynamic member" in importer_text and "mapping with struct key" in importer_text, - "importer rejects dynamic members and struct mapping keys") - - with tempfile.TemporaryDirectory(prefix="verity-struct-check-", dir=ROOT.parent) as directory: - root = Path(directory) - for name in ("Verity", "Compiler", "Contracts", "scripts"): - shutil.copytree(ROOT / name, root / name) - for name in ("lakefile.lean", "lake-manifest.json", "lean-toolchain"): - shutil.copy2(ROOT / name, root / name) - shutil.copytree(ROOT / ".lake/build", root / ".lake/build") - (root / ".lake/solidity-import").mkdir(parents=True) - shutil.copy2(ROOT / ".lake/solidity-import/solc", root / ".lake/solidity-import/solc") - (root / ".lake/packages").symlink_to(ROOT / ".lake/packages", target_is_directory=True) - - source = root / SMOKE / "Structs.sol" - original = source.read_bytes() - source_stamp = source.stat() - importer = root / "Contracts/VaultFromSolidity/Importer/Importer.lean" - importer_original = importer.read_bytes() - importer_stamp = importer.stat() - proof = root / PROOF - proof_text = proof.read_text() - theorem_names = re.findall(r"^theorem\s+(\w+)", proof_text, re.M) - - def edit_source(data: bytes) -> None: - source.write_bytes(data) - os.utime(source, ns=(source_stamp.st_atime_ns, source_stamp.st_mtime_ns)) - - def build(success: bool = True, contains: str | None = None) -> str: - return run(root, [LAKE, "build", TARGET], success, contains) - - def artifacts() -> dict[str, tuple[int, str]]: - paths = list((root / ".lake/build/lib/lean/Contracts/SolidityImportSmoke/Structs").rglob("*.olean")) - return { - str(path.relative_to(root)): ( - path.stat().st_mtime_ns, - hashlib.sha256(path.read_bytes()).hexdigest(), - ) - for path in paths - } - - def print_names(*names: str) -> str: - probe = root / ".lake/solidity-import/StructPrint.lean" - try: - probe.write_text( - f"import {SMOKE.replace('/', '.')}.Structs\n" - + "".join(f"#print {NS}.{name}\n" for name in names) - ) - return run(root, [LAKE, "env", "lean", str(probe)]) - finally: - probe.unlink(missing_ok=True) - - def slot_numbers() -> str: - slot_probe = root / ".lake/solidity-import/StructSlotProbe.lean" - try: - slot_probe.write_text( - f"import {SMOKE.replace('/', '.')}.Structs\n" - f"#eval [{NS}.data_amountSlot.slot, {NS}.data_whoSlot.slot, {NS}.otherSlot.slot]\n" - ) - found = re.findall(r"\[\d+(?:, \d+)*\]", run(root, [LAKE, "env", "lean", str(slot_probe)])) - if not found: - raise AssertionError("slot probe produced no list") - return found[0] - finally: - slot_probe.unlink(missing_ok=True) - - build() - check(True, "baseline lake build SolidityImportSmokeStructs") - - audit_file = root / ".lake/solidity-import/StructAxiomAudit.lean" - try: - audit_file.write_text( - f"import {SMOKE.replace('/', '.')}.Proofs\n" - + "\n".join( - f"#print axioms {SMOKE.replace('/', '.')}.Proofs.{name}" - for name in theorem_names - ) - + "\n" - ) - audit = run(root, [LAKE, "env", "lean", str(audit_file)]) - finally: - audit_file.unlink(missing_ok=True) - entries = re.findall( - r"'Contracts.SolidityImportSmoke.Structs.Proofs.(\w+)' depends on axioms: \[([^\]]*)\]", - audit, - ) - check(set(theorem_names) == {name for name, _ in entries}, - "every struct theorem appears in #print axioms output") - axioms = {a.strip() for _, values in entries for a in values.split(",") if a.strip()} - check(axioms <= {"propext", "Quot.sound"}, - "no project axioms or sorryAx: " + ", ".join(sorted(axioms))) - - printed = print_names("set", "get", "make", "makeRev") - check(".fst" in printed and ".snd" in printed, - "#print Store.set decodes struct members") - check(".pair" in printed, - "#print Store.make / Store.get encode the user struct") - check(".pairRev" in printed, - "#print Store.makeRev encodes named Acc({who, amount})") - check(slot_numbers() == "[0, 1, 2]", - "struct members occupy consecutive solc slots [amount, who, other]") - - def broken_theorems(output: str) -> set[str]: - error_lines = [int(value) for value in re.findall(rf"{PROOF}:(\d+):", output)] - starts = [ - (match.group(1), line_no) - for line_no, line in enumerate(proof_text.splitlines(), 1) - if (match := re.match(r"theorem\s+([A-Za-z0-9_']+)", line.strip())) - ] - ranges = { - name: (start, starts[index + 1][1] - 1 if index + 1 < len(starts) - else len(proof_text.splitlines())) - for index, (name, start) in enumerate(starts) - } - return {name for name, (start, end) in ranges.items() - if any(start <= line <= end for line in error_lines)} - - dropped_who = original.replace( - b" data = a;\n", - b" data = Acc({amount: a.amount, who: data.who});\n", - ) - check(original != dropped_who, "dropped who write has a source target") - edit_source(dropped_who) - output = build(False, "Contracts.SolidityImportSmoke.Structs.Proofs") - check("set_then_get" in broken_theorems(output), - "dropping the who write breaks set_then_get") - edit_source(original) - build() - - reordered = original.replace( - b"struct Acc {\n uint256 amount;\n address who;\n}", - b"struct Acc {\n address who;\n uint256 amount;\n}", - ) - check(original != reordered, "struct member reorder has a source target") - edit_source(reordered) - output = build(False, "unsupported struct shape") - check(re.search(r"Contracts/SolidityImportSmoke/Structs/Structs.sol:\d+:\d+:", output) - is not None, - "reordering pair members is rejected with a source position") - edit_source(original) - build() - - named_reorder = original.replace( - b" return Acc({amount: amount, who: who});", - b" return Acc({who: who, amount: amount});", - ) - check(original != named_reorder, "named constructor reorder has a source target") - edit_source(named_reorder) - build() - printed_rev = print_names("make") - check(".pairRev" in printed_rev, - "named Acc({who, amount}) imports as pairRev") - check(True, "named Acc({who: who, amount: amount}) still imports") - edit_source(original) - build() - printed_fwd = print_names("make") - check(".pair" in printed_fwd and ".pairRev" not in printed_fwd, - "named Acc({amount, who}) stays Expr.pair") - - positional = original.replace( - b" return Acc({amount: amount, who: who});", - b" return Acc(amount, who);", - ) - check(original != positional, "positional constructor has a source target") - edit_source(positional) - build() - printed_pos = print_names("make") - check(".pair" in printed_pos and ".pairRev" not in printed_pos, - "positional Acc(amount, who) stays AST/member order as Expr.pair") - edit_source(original) - build() - - before = artifacts() - importer.write_bytes(importer_original + b"\n-- structs identity probe\n") - os.utime(importer, ns=(importer_stamp.st_atime_ns, importer_stamp.st_mtime_ns)) - build() - check(before != artifacts(), "Lean importer content change invalidates struct artifacts") - importer.write_bytes(importer_original) - build() - - dyn_src = original.replace( - b" address who;\n", - b" address who;\n string note;\n", - ).replace( - b" return Acc({amount: amount, who: who});", - b" return Acc({amount: amount, who: who, note: \"\"});", - ).replace( - b" return Acc({who: who, amount: amount});", - b" return Acc({who: who, amount: amount, note: \"\"});", - ) - check(dyn_src != original, "dynamic member mutation has a source target") - edit_source(dyn_src) - output = build(False) - check( - "struct with a dynamic member" in output or "unsupported struct shape" in output, - "dynamic member is rejected", - ) - check(re.search(r"Contracts/SolidityImportSmoke/Structs/Structs.sol:\d+:\d+:", output) - is not None, - "dynamic member rejected with source position") - edit_source(original) - - key_old = b" Acc public data;\n uint256 public other;\n" - key_new = b" Acc public data;\n uint256 public other;\n mapping(Acc => uint256) private keyed;\n" - check(original.count(key_old) == 1, "struct mapping key mutation has one source target") - edit_source(original.replace(key_old, key_new)) - output = build(False) - check( - "mapping with struct key" in output or "TypeError" in output, - "mapping with struct key is rejected", - ) - check(re.search(r"Contracts/SolidityImportSmoke/Structs/Structs.sol:\d+:\d+:", output) - is not None, - "mapping with struct key rejected with source position") - edit_source(original) - build() - - check(source.read_bytes() == original and importer.read_bytes() == importer_original, - "temporary mutations restored; final baseline passes") - print(f"PASS all struct-slice acceptance checks ({len(theorem_names)} audited theorems)", - flush=True) - - -if __name__ == "__main__": - main() diff --git a/Contracts/VaultFromSolidity/Importer/Importer.lean b/Contracts/VaultFromSolidity/Importer/Importer.lean deleted file mode 100644 index a3bfd56460..0000000000 --- a/Contracts/VaultFromSolidity/Importer/Importer.lean +++ /dev/null @@ -1,1915 +0,0 @@ -import Lean -import Verity.Stdlib.Math -import Verity.Core.SolidityImportAttr -import Compiler.Sha256.Engine -import Contracts.VaultFromSolidity.Importer.Semantics - -/-! -A proof-only Solidity frontend. This module invokes pinned `solc --standard-json`, -validates a closed typed-AST/storage-layout subset, parses it into the closed, -intrinsically typed inductive in `Syntax.lean`, and registers each entry point as -`Semantics.lean`'s `Fn.meaning` applied to that parsed term. The accepted subset is -a kernel-checked Lean value, so nothing is serialized, no intermediate IR is -materialised, and no Lean source is generated or written to disk. - -Besides the executable model it elaborates a kernel-checked `Storage` structure -(the one use of the standard command elaborator in this frontend), registers -`view : ContractState → Storage` from the `Slot` handles, tags imported -declarations into the `solidity_import` simp set, and registers a deterministic -entry-point relation `step`. Specs can then say `v.totalAssets` instead of naming -a raw slot number. --/ - -open Lean Meta Elab Command - -namespace SolidityImporter - -/-- Release identity used in `sourceDigest`. Platform banners are not part of it. -/ -private def solcVersionPin := "0.8.33+commit.64118f21" -/-- Official SHA-256 digests from binaries.soliditylang.org `/list.json`. -/ -private def officialSolcSha256s : Array String := #[ - "1274e5c4621ae478090c5a1f48466fd3c5f658ed9e14b15a0b213dc806215468", - "8324280591ce398d7e2722846bc10ecf1779b13a328ef97b687c92cd9c70801a"] -private def acceptedSolcVersionOutputs : Array String := #[ - s!"solc, the solidity compiler commandline interface\nVersion: {solcVersionPin}.Linux.g++", - s!"solc, the solidity compiler commandline interface\nVersion: {solcVersionPin}.Darwin.appleclang"] -/-- Logical path × package-relative path. The digest covers this file, so adding -a source moves every imported `sourceDigest`. -/ -private def registeredSources : List (String × String) := [ - ("Contracts/VaultFromSolidity/Vault.sol", "Contracts/VaultFromSolidity/Vault.sol"), - ("Contracts/SolidityImportSmoke/Inheritance/Inheritance.sol", - "Contracts/SolidityImportSmoke/Inheritance/Inheritance.sol"), - ("Contracts/SolidityImportSmoke/Modifiers/Modifiers.sol", - "Contracts/SolidityImportSmoke/Modifiers/Modifiers.sol"), - ("Contracts/SolidityImportSmoke/Structs/Structs.sol", - "Contracts/SolidityImportSmoke/Structs/Structs.sol") -] - -private def field (j : Json) (key : String) : MetaM Json := - match j.getObjVal? key with - | .ok v => pure v - | .error e => throwError "{e}" - -private def field? (j : Json) (key : String) : Option Json := - (j.getObjVal? key).toOption - -private def str (j : Json) : MetaM String := - match j.getStr? with - | .ok v => pure v - | .error e => throwError "{e}" - -private def nat (j : Json) : MetaM Nat := - match j.getNat? with - | .ok v => pure v - | .error e => throwError "{e}" - -private def int (j : Json) : MetaM Int := - match j.getInt? with - | .ok v => pure v - | .error e => throwError "{e}" - -private def bool (j : Json) : MetaM Bool := - match j.getBool? with - | .ok v => pure v - | .error e => throwError "{e}" - -private def arr (j : Json) : MetaM (Array Json) := - match j.getArr? with - | .ok v => pure v - | .error e => throwError "{e}" - -private def objKeys (j : Json) : MetaM (List String) := - match j with - | .obj o => pure <| o.foldl (fun keys key _ => key :: keys) [] - | _ => throwError "object expected" - -private def requireKeys (j : Json) (allowed : List String) (what : String) : MetaM Unit := do - for key in ← objKeys j do - unless key ∈ allowed do throwError "unknown {what} field {key}" - -private def nodeKind (j : Json) : MetaM String := field j "nodeType" >>= str -private def nodeId (j : Json) : MetaM Nat := field j "id" >>= nat - -private partial def collectAstIds (j : Json) : MetaM (List Nat) := do - match j with - | .obj o => - let self ← if (field? j "nodeType").isSome then do pure [← nodeId j] else pure [] - let mut ids := self - for (_, value) in o.toList do ids := ids ++ (← collectAstIds value) - pure ids - | .arr xs => - let mut ids := [] - for value in xs do ids := ids ++ (← collectAstIds value) - pure ids - | _ => pure [] - -private def expect (ok : Bool) (message : String) : MetaM Unit := - unless ok do throwError message - -private def hexDigit (n : Nat) : Char := - if n < 10 then Char.ofNat ('0'.toNat + n) else Char.ofNat ('a'.toNat + n - 10) - -private def sha256Hex (bytes : ByteArray) : String := - (Sha256Engine.sha256 bytes).data.foldl (init := "") fun acc byte => - acc.push (hexDigit (byte.toNat / 16)) |>.push (hexDigit (byte.toNat % 16)) - -private def verifyCompiler (compiler : System.FilePath) : MetaM Unit := do - let output ← - if System.Platform.isOSX then - IO.Process.output { cmd := "/usr/bin/shasum", args := #["-a", "256", compiler.toString] } - else - IO.Process.output { cmd := "/usr/bin/sha256sum", args := #[compiler.toString] } - unless output.exitCode == 0 && officialSolcSha256s.contains (output.stdout.take 64).toString do - throwError "compiler checksum mismatch" - -private structure SourceContext where - path : System.FilePath - logicalPath : String - bytes : ByteArray - sourceId : Nat - -private def parseSpan (j : Json) : MetaM (Nat × Nat × Nat) := do - let pieces := (← str (← field j "src")).splitOn ":" - match pieces with - | [a, b, c] => - let some start := a.toNat? | throwError "invalid source span" - let some size := b.toNat? | throwError "invalid source span" - let some sourceId := c.toNat? | throwError "invalid source span" - pure (start, size, sourceId) - | _ => throwError "invalid source span" - -private def failAt (ctx : SourceContext) (j : Json) (why : String) : MetaM α := do - let (start, size, _sourceId) ← parseSpan j - let before := ctx.bytes.data.extract 0 (min start ctx.bytes.size) - let (line, column) := before.foldl - (fun (p : Nat × Nat) b => if b == 10 then (p.1 + 1, 1) else (p.1, p.2 + 1)) (1, 1) - let excerptBytes : ByteArray := ⟨ctx.bytes.data.extract start (min (start + size) (min ctx.bytes.size (start + 100)))⟩ - let excerpt := String.fromUTF8? excerptBytes |>.getD "" - let kind := (← nodeKind j) - throwError "{ctx.logicalPath}:{line}:{column}: {kind}: {why}\n{excerpt}" - -private def needAt (ctx : SourceContext) (j : Json) (ok : Bool) (why : String) : MetaM Unit := - unless ok do failAt ctx j why - -private def commonFields := ["id", "src", "nodeType"] -private def expressionFields := ["isConstant", "isLValue", "isPure", "lValueRequested", "typeDescriptions"] - -private def allowedNodeFields : String → Option (List String) - | "SourceUnit" => some ["absolutePath", "exportedSymbols", "license", "nodes"] - | "PragmaDirective" => some ["literals"] - | "ContractDefinition" => some ["abstract", "baseContracts", "canonicalName", "contractDependencies", - "contractKind", "documentation", "fullyImplemented", "linearizedBaseContracts", "name", "nameLocation", - "nodes", "scope", "usedErrors", "usedEvents", "storageLayout"] - | "StructuredDocumentation" => some ["text"] - | "VariableDeclaration" => some ["constant", "functionSelector", "mutability", "name", "nameLocation", "scope", - "stateVariable", "storageLocation", "typeDescriptions", "typeName", "visibility", "value"] - | "ElementaryTypeName" => some ["name", "stateMutability", "typeDescriptions"] - | "Mapping" => some ["keyName", "keyNameLocation", "keyType", "typeDescriptions", "valueName", - "valueNameLocation", "valueType"] - | "ErrorDefinition" => some ["errorSelector", "name", "nameLocation", "parameters"] - | "FunctionDefinition" => some ["body", "functionSelector", "implemented", "kind", "modifiers", "name", - "nameLocation", "parameters", "returnParameters", "scope", "stateMutability", "virtual", "visibility", - "documentation", "overrides", "baseFunctions"] - | "InheritanceSpecifier" => some ["baseName", "arguments"] - | "IdentifierPath" => some ["name", "nameLocations", "referencedDeclaration"] - | "OverrideSpecifier" => some ["overrides"] - | "ParameterList" => some ["parameters"] - | "Block" => some ["statements"] - | "ExpressionStatement" => some ["expression"] - | "Assignment" => some (expressionFields ++ ["leftHandSide", "operator", "rightHandSide"]) - | "BinaryOperation" => some (expressionFields ++ ["commonType", "leftExpression", "operator", "rightExpression", "function"]) - | "Identifier" => some ["argumentTypes", "name", "overloadedDeclarations", "referencedDeclaration", "typeDescriptions"] - | "MemberAccess" => some (expressionFields ++ ["expression", "memberLocation", "memberName", - "referencedDeclaration", "argumentTypes"]) - | "IndexAccess" => some (expressionFields ++ ["baseExpression", "indexExpression"]) - | "Literal" => some (expressionFields ++ ["hexValue", "kind", "subdenomination", "value"]) - | "FunctionCall" => some (expressionFields ++ ["arguments", "expression", "kind", "nameLocations", "names", "tryCall"]) - | "VariableDeclarationStatement" => some ["assignments", "declarations", "initialValue"] - | "IfStatement" => some ["condition", "trueBody", "falseBody"] - | "RevertStatement" => some ["errorCall"] - | "Return" => some ["expression", "functionReturnParameters"] - | "ModifierDefinition" => some ["body", "name", "nameLocation", "parameters", "virtual", "visibility", - "documentation"] - | "ModifierInvocation" => some ["arguments", "kind", "modifierName"] - | "PlaceholderStatement" => some ([] : List String) - | "StructDefinition" => some ["canonicalName", "members", "name", "nameLocation", "scope", "visibility"] - | "UserDefinedTypeName" => some ["pathNode", "referencedDeclaration", "typeDescriptions"] - | _ => none - -private def requiredNodeFields : String → List String - | "SourceUnit" => ["absolutePath", "exportedSymbols", "license", "nodes"] - | "PragmaDirective" => ["literals"] - | "ContractDefinition" => ["abstract", "baseContracts", "canonicalName", "contractDependencies", - "contractKind", "fullyImplemented", "linearizedBaseContracts", "name", "nameLocation", "nodes", - "scope", "usedErrors", "usedEvents"] - | "StructuredDocumentation" => ["text"] - | "VariableDeclaration" => ["constant", "mutability", "name", "nameLocation", "scope", "stateVariable", - "storageLocation", "typeDescriptions", "typeName", "visibility"] - | "ElementaryTypeName" => ["name", "typeDescriptions"] - | "Mapping" => ["keyName", "keyNameLocation", "keyType", "typeDescriptions", "valueName", - "valueNameLocation", "valueType"] - | "ErrorDefinition" => ["errorSelector", "name", "nameLocation", "parameters"] - | "FunctionDefinition" => ["implemented", "kind", "modifiers", "name", - "nameLocation", "parameters", "returnParameters", "scope", "stateMutability", "virtual", "visibility"] - | "InheritanceSpecifier" => ["baseName"] - | "IdentifierPath" => ["name", "nameLocations", "referencedDeclaration"] - | "OverrideSpecifier" => ["overrides"] - | "ParameterList" => ["parameters"] - | "Block" => ["statements"] - | "ExpressionStatement" => ["expression"] - | "Assignment" => expressionFields ++ ["leftHandSide", "operator", "rightHandSide"] - | "BinaryOperation" => expressionFields ++ ["commonType", "leftExpression", "operator", "rightExpression"] - | "Identifier" => ["name", "overloadedDeclarations", "referencedDeclaration", "typeDescriptions"] - | "MemberAccess" => expressionFields ++ ["expression", "memberLocation", "memberName", "typeDescriptions"] - | "IndexAccess" => expressionFields ++ ["baseExpression", "indexExpression", "typeDescriptions"] - | "Literal" => expressionFields ++ ["hexValue", "kind", "value", "typeDescriptions"] - | "FunctionCall" => expressionFields ++ ["arguments", "expression", "kind", "nameLocations", "names", - "tryCall", "typeDescriptions"] - | "VariableDeclarationStatement" => ["assignments", "declarations", "initialValue"] - | "IfStatement" => ["condition", "trueBody"] - | "RevertStatement" => ["errorCall"] - | "Return" => ["expression", "functionReturnParameters"] - | "ModifierDefinition" => ["name", "nameLocation", "parameters", "virtual", "visibility"] - | "ModifierInvocation" => ["kind", "modifierName"] - | "PlaceholderStatement" => [] - | "StructDefinition" => ["canonicalName", "members", "name", "nameLocation", "scope", "visibility"] - | "UserDefinedTypeName" => ["pathNode", "referencedDeclaration", "typeDescriptions"] - | _ => [] - -private def childFields : List String := ["nodes", "baseContracts", "parameters", "returnParameters", "body", - "statements", "typeName", "keyType", "valueType", "modifiers", "overrides", "storageLayout", "leftHandSide", - "rightHandSide", "leftExpression", "rightExpression", "expression", "baseExpression", "indexExpression", - "arguments", "declarations", "initialValue", "condition", "trueBody", "falseBody", "errorCall", "baseName", - "members", "modifierName", "pathNode"] - -private def validateTypeDescription (j : Json) : MetaM Unit := do - requireKeys j ["typeIdentifier", "typeString"] "type description" - let _ ← str (← field j "typeIdentifier") - let _ ← str (← field j "typeString") - -private def validateStringArray (j : Json) : MetaM Unit := do - for value in ← arr j do let _ ← str value - -private def validateNatArray (j : Json) : MetaM Unit := do - for value in ← arr j do let _ ← nat value - -private def validateAssignments (j : Json) : MetaM Unit := do - for value in ← arr j do unless value.isNull do let _ ← nat value - -private def validateExportedSymbols (j : Json) : MetaM Unit := do - match j with - | .obj entries => for (_, ids) in entries.toList do validateNatArray ids - | _ => throwError "expected exported-symbol object" - -private def validateMetadataField (ctx : SourceContext) (node : Json) (kind key : String) (value : Json) : MetaM Unit := do - if ["absolutePath", "canonicalName", "contractKind", "text", "mutability", "name", - "nameLocation", "scope", "storageLocation", "visibility", "stateMutability", "keyName", - "keyNameLocation", "valueName", "valueNameLocation", "errorSelector", "kind", "operator", - "memberLocation", "memberName", "hexValue", "value"].contains key then - if key == "scope" then let _ ← nat value - else let _ ← str value - else if ["abstract", "fullyImplemented", "constant", "stateVariable", "indexed", "implemented", - "virtual", "isConstant", "isLValue", "isPure", "lValueRequested", "tryCall"].contains key then - let _ ← bool value - else if key == "referencedDeclaration" then - unless value.isNull do let _ ← int value - else if key == "functionReturnParameters" then - let _ ← nat value - else if ["contractDependencies", "linearizedBaseContracts", "usedErrors", "usedEvents", - "baseFunctions", "overloadedDeclarations"].contains key then - validateNatArray value - else if ["literals", "nameLocations", "names"].contains key then - validateStringArray value - else if key == "assignments" then validateAssignments value - else if key == "exportedSymbols" then validateExportedSymbols value - else if ["license", "functionSelector", "subdenomination"].contains key then - unless value.isNull do let _ ← str value - else - failAt ctx node ("unvalidated AST metadata field " ++ kind ++ "." ++ key) - -private partial def validateNode (ctx : SourceContext) (j : Json) : MetaM Unit := do - let kind ← nodeKind j - let some allowed := allowedNodeFields kind | failAt ctx j "unsupported AST node" - let keys ← objKeys j - needAt ctx j (keys.all fun key => commonFields.contains key || allowed.contains key) - ("unexpected AST fields: " ++ String.intercalate ", " (keys.filter fun k => !(commonFields.contains k || allowed.contains k))) - let missing := (requiredNodeFields kind).filter fun key => !(keys.contains key) - needAt ctx j missing.isEmpty ("missing AST fields: " ++ String.intercalate ", " missing) - let _ ← nodeId j - let (start, size, sourceId) ← parseSpan j - needAt ctx j (sourceId == ctx.sourceId && start <= ctx.bytes.size && size <= ctx.bytes.size - start) - "source span outside registered source" - if let some description := field? j "typeDescriptions" then - validateTypeDescription description - if let some common := field? j "commonType" then - unless common.isNull do validateTypeDescription common - if let some arguments := field? j "argumentTypes" then - unless arguments.isNull do - for description in ← arr arguments do validateTypeDescription description - if let some documentation := field? j "documentation" then - if documentation.isNull then pure () - else match documentation with - | .str _ => pure () - | .obj _ => - needAt ctx j ((← nodeKind documentation) == "StructuredDocumentation") "invalid documentation" - validateNode ctx documentation - | _ => failAt ctx j "invalid documentation" - for key in keys do - unless commonFields.contains key || childFields.contains key || - ["typeDescriptions", "commonType", "argumentTypes", "documentation"].contains key || - (kind == "VariableDeclaration" && key == "value") do - try validateMetadataField ctx j kind key (← field j key) - catch _ => failAt ctx j ("invalid AST metadata field " ++ kind ++ "." ++ key) - if kind == "ContractDefinition" then - needAt ctx j ((field? j "storageLayout").all Json.isNull) "contract layout at specifier unsupported" - if kind == "VariableDeclaration" then - needAt ctx j ((field? j "value").all Json.isNull && (field? j "overrides").all Json.isNull) - "initializer/override unsupported" - if kind == "BinaryOperation" then - needAt ctx j ((field? j "function").all Json.isNull) "user-defined operator unsupported" - if kind == "FunctionCall" then - let k ← str (← field j "kind") - needAt ctx j ((k == "functionCall" || k == "structConstructorCall") && - !(← bool (← field j "tryCall"))) "unsupported call surface" - if k == "functionCall" then - needAt ctx j ((← arr (← field j "names")).isEmpty) "unsupported call surface" - for key in childFields do - if let some value := field? j key then - if key == "storageLayout" && kind == "ContractDefinition" then pure () - else if value.isNull then pure () - else match value with - | .arr values => for child in values do validateNode ctx child - | .obj _ => validateNode ctx value - | _ => failAt ctx j ("invalid AST child: " ++ key) - let requireKind (key : String) (kinds : List String) : MetaM Unit := do - let child ← field j key - needAt ctx j (kinds.contains (← nodeKind child)) ("unexpected child kind: " ++ key) - match kind with - | "SourceUnit" => - let _ ← arr (← field j "nodes") - | "ContractDefinition" => - let _ ← arr (← field j "nodes"); let _ ← arr (← field j "baseContracts") - | "InheritanceSpecifier" => - requireKind "baseName" ["IdentifierPath"] - if let some arguments := field? j "arguments" then - needAt ctx j ((← arr arguments).isEmpty) "constructor arguments unsupported" - | "OverrideSpecifier" => - let _ ← arr (← field j "overrides") - | "FunctionDefinition" => - match field? j "body" with - | none => - needAt ctx j (!(← bool (← field j "implemented")) && (← bool (← field j "virtual"))) - "unimplemented function must be virtual" - | some body => - if body.isNull then - needAt ctx j (!(← bool (← field j "implemented")) && (← bool (← field j "virtual"))) - "unimplemented function must be virtual" - else requireKind "body" ["Block"] - requireKind "parameters" ["ParameterList"] - requireKind "returnParameters" ["ParameterList"] - let _ ← arr (← field j "modifiers") - | "ParameterList" => - for p in (← arr (← field j "parameters")) do - needAt ctx j ((← nodeKind p) == "VariableDeclaration") "invalid parameter declaration" - | "Block" => let _ ← arr (← field j "statements") - | "VariableDeclaration" => requireKind "typeName" ["ElementaryTypeName", "Mapping", "UserDefinedTypeName"] - | "Mapping" => - let keyType ← field j "keyType" - if (← nodeKind keyType) == "UserDefinedTypeName" then - failAt ctx j "mapping with struct key" - requireKind "keyType" ["ElementaryTypeName"] - requireKind "valueType" ["ElementaryTypeName", "UserDefinedTypeName"] - | "ExpressionStatement" => let _ ← field j "expression" - | "Assignment" => let _ ← field j "leftHandSide"; let _ ← field j "rightHandSide" - | "BinaryOperation" => let _ ← field j "leftExpression"; let _ ← field j "rightExpression" - | "MemberAccess" => let _ ← field j "expression" - | "IndexAccess" => let _ ← field j "baseExpression"; let _ ← field j "indexExpression" - | "FunctionCall" => let _ ← field j "expression"; let _ ← arr (← field j "arguments") - | "VariableDeclarationStatement" => - for d in (← arr (← field j "declarations")) do - needAt ctx j ((← nodeKind d) == "VariableDeclaration") "invalid local declaration" - let _ ← field j "initialValue" - | "IfStatement" => - let trueBody ← field j "trueBody" - let tk ← nodeKind trueBody - needAt ctx j (tk == "Block" || tk == "RevertStatement") "unexpected child kind: trueBody" - | "RevertStatement" => requireKind "errorCall" ["FunctionCall"] - | "Return" => - if let some expr := field? j "expression" then - unless expr.isNull do let _ ← field j "expression" - | "ErrorDefinition" => requireKind "parameters" ["ParameterList"] - | "ModifierDefinition" => - requireKind "parameters" ["ParameterList"] - match field? j "body" with - | none => failAt ctx j "modifier missing body" - | some body => - if body.isNull then failAt ctx j "modifier missing body" - else requireKind "body" ["Block"] - | "ModifierInvocation" => - requireKind "modifierName" ["IdentifierPath"] - if let some arguments := field? j "arguments" then - unless arguments.isNull do let _ ← arr arguments - | "PlaceholderStatement" => pure () - | "StructDefinition" => - let members ← arr (← field j "members") - needAt ctx j (members.size == 2) "unsupported struct shape" - for m in members do - needAt ctx j ((← nodeKind m) == "VariableDeclaration") "invalid struct member" - let typ ← str (← field (← field m "typeDescriptions") "typeString") - unless typ == "uint256" || typ == "address" do - failAt ctx m "struct with a dynamic member" - | "UserDefinedTypeName" => requireKind "pathNode" ["IdentifierPath"] - | _ => pure () - -private structure StructMemberInfo where - name : String - ty : Sol.Ty - -private structure StructInfo where - id : Nat - name : String - canonicalName : String - members : List StructMemberInfo - -private structure FieldInfo where - id : Nat - var : String - name : String - getter : Option String - slot : Nat - sty : Sol.StorageTy - structId : Option Nat - memberIndex : Option Nat - -private structure OpaqueField where - id : Nat - label : String - slot : Nat - typeLabel : String - -private structure ModInfo where - id : Nat - name : String - contractId : Nat - node : Json - -private structure FnInfo where - id : Nat - contractId : Nat - contractName : String - name : String - visibility : String - mutability : String - virtual : Bool - implemented : Bool - isTarget : Bool - paramTys : List Sol.Ty - paramIds : List (Nat × Sol.Ty × String) - rets : List Sol.Ty - namedReturn : Option (Nat × Sol.Ty × String) - node : Json - -private def FnInfo.sig (f : FnInfo) : Sol.Sig := ⟨f.paramTys.reverse, f.rets⟩ - -private def FnInfo.leanIdent (f : FnInfo) : String := - if f.isTarget && (f.visibility == "public" || f.visibility == "external") then f.name - else f.contractName ++ "_" ++ f.name - -private def FnInfo.isEntry (f : FnInfo) : Bool := - f.implemented && (f.visibility == "public" || f.visibility == "external") - -/-- `view`/`pure` may appear in expression position. Nonpayable internals are -statement-only (`Stmt.callStmt`): pinned solc 0.8.x legacy codegen evaluates -an effectful call before the other operand / `+=` old-read. -/ -private def FnInfo.viewOrPure (f : FnInfo) : Bool := - f.mutability == "view" || f.mutability == "pure" - -private structure Frontend where - source : SourceContext - ast : Json - targetName : String - targetId : Nat - linearization : List Nat - contractById : List (Nat × String × Json) - fields : List FieldInfo - opaqueEntries : List OpaqueField - errors : List (Nat × String) - structs : List StructInfo - modifiers : List ModInfo - allFns : List FnInfo - functions : List FnInfo - digest : String - -private def validName (name : String) : Bool := - match name.toList with - | [] => false - | c :: cs => (c.isAlpha || c == '_') && (c.isAlpha || cs.any (·.isAlpha)) && - cs.all (fun c => c.isAlphanum || c == '_') && - !["sourceDigest", "Storage", "view", "step", "opaqueFields", "registeredSources"].contains name - -private def identifier (ctx : SourceContext) (j : Json) : MetaM String := do - let name ← str (← field j "name") - needAt ctx j (validName name) "unsupported/reserved name" - pure name - -private def typeString (j : Json) : MetaM String := - field j "typeDescriptions" >>= (field · "typeString") >>= str - -private def parseStructMembers (ctx : SourceContext) (node : Json) : MetaM (List StructMemberInfo) := do - let mut members : List StructMemberInfo := [] - for m in (← arr (← field node "members")) do - needAt ctx m (!(← bool (← field m "stateVariable")) && - !(← bool (← field m "constant")) && - (← str (← field m "mutability")) == "mutable" && - (← str (← field m "storageLocation")) == "default") "unsupported struct member" - let typ ← typeString m - let ty ← match typ with - | "uint256" => pure Sol.Ty.uint - | "address" => pure Sol.Ty.addr - | _ => failAt ctx m "struct with a dynamic member" - let name ← identifier ctx m - members := members ++ [StructMemberInfo.mk name ty] - needAt ctx node (!members.isEmpty) "empty struct unsupported" - needAt ctx node (members.map (·.ty) == [.uint, .addr]) "unsupported struct shape" - pure members - -private def lookupStruct (structs : List StructInfo) (id : Nat) : Option StructInfo := - structs.find? (·.id == id) - -private def parseValueTy (ctx : SourceContext) (structs : List StructInfo) (j : Json) : MetaM Sol.Ty := do - let typ ← typeString j - match typ with - | "uint256" => pure .uint - | "address" => pure .addr - | _ => - if typ.startsWith "struct " then - let some typeName := field? j "typeName" | failAt ctx j "unsupported value type" - needAt ctx j ((← nodeKind typeName) == "UserDefinedTypeName") "unsupported value type" - let rid ← int (← field typeName "referencedDeclaration") - needAt ctx j (rid >= 0) "unresolved struct" - let some info := lookupStruct structs rid.toNat | failAt ctx j "using an undeclared struct" - needAt ctx j (info.members.map (·.ty) == [.uint, .addr]) "unsupported struct shape" - pure (.pair info.id) - else failAt ctx j "unsupported value type" - -private def parseFnInfo (ctx : SourceContext) (structs : List StructInfo) - (contractId : Nat) (contractName : String) - (isTarget : Bool) (node : Json) : MetaM FnInfo := do - let name ← identifier ctx node - let mutability ← str (← field node "stateMutability") - needAt ctx node ((← str (← field node "kind")) == "function" && - ["internal", "public", "external", "private"].contains (← str (← field node "visibility")) && - ["nonpayable", "view", "pure"].contains mutability) - "unsupported function surface" - let implemented ← bool (← field node "implemented") - let virt ← bool (← field node "virtual") - if implemented then - needAt ctx node ((field? node "body").any fun b => !b.isNull) "implemented function missing body" - else - needAt ctx node virt "unimplemented function must be virtual" - let mut paramTys : List Sol.Ty := [] - let mut paramIds : List (Nat × Sol.Ty × String) := [] - for p in (← arr (← field (← field node "parameters") "parameters")) do - let loc ← str (← field p "storageLocation") - needAt ctx node (!(← bool (← field p "stateVariable")) && - !(← bool (← field p "constant")) && - (← str (← field p "mutability")) == "mutable" && - (loc == "default" || loc == "memory") && - (← str (← field p "visibility")) == "internal" && - (field? p "value").all Json.isNull) "unsupported parameter declaration" - let ty ← parseValueTy ctx structs p - match ty with - | .pair _ => needAt ctx p (loc == "memory") "unsupported parameter declaration" - | _ => needAt ctx p (loc == "default") "unsupported parameter declaration" - let pname ← identifier ctx p - paramTys := paramTys ++ [ty] - paramIds := paramIds ++ [(← nodeId p, ty, pname)] - let rs ← arr (← field (← field node "returnParameters") "parameters") - needAt ctx node (rs.size <= 1) "unsupported signature" - let mut rets : List Sol.Ty := [] - let mut namedReturn : Option (Nat × Sol.Ty × String) := none - for r in rs do - let loc ← str (← field r "storageLocation") - needAt ctx node (!(← bool (← field r "stateVariable")) && - !(← bool (← field r "constant")) && - (← str (← field r "mutability")) == "mutable" && - (loc == "default" || loc == "memory") && - (← str (← field r "visibility")) == "internal" && - (field? r "value").all Json.isNull) "unsupported parameter declaration" - let ty ← parseValueTy ctx structs r - match ty with - | .pair _ => needAt ctx r (loc == "memory") "unsupported return type" - | .uint => needAt ctx r (loc == "default") "unsupported return type" - | .addr => needAt ctx r (loc == "default") "unsupported return type" - rets := [ty] - let rname ← str (← field r "name") - if !rname.isEmpty then - needAt ctx node (validName rname) "unsupported/reserved name" - namedReturn := some (← nodeId r, ty, rname) - pure { - id := ← nodeId node, contractId, contractName, name, - visibility := ← str (← field node "visibility"), - mutability, virtual := virt, implemented, isTarget, paramTys, paramIds, rets, namedReturn, node - } - -set_option maxRecDepth 2048 in -private def parseCompilerOutput (sourcePath : System.FilePath) (logicalPath : String) - (raw : ByteArray) (outputText importerText : String) (targetName? : Option String) : - MetaM Frontend := do - let output ← match Json.parse outputText with - | .ok j => pure j - | .error e => throwError "invalid solc standard JSON: {e}" - requireKeys output ["contracts", "sources", "errors"] "solc output" - if let some errors := field? output "errors" then - for e in (← arr errors) do - -- Compiler diagnostics may grow extra locator fields; only the fields we - -- read are required. Unknown *AST* fields still fail closed. - let keys ← objKeys e - for need in ["formattedMessage", "message", "severity"] do - unless keys.contains need do throwError "missing compiler diagnostic field {need}" - if (← str (← field e "severity")) == "error" then - throwError "{← str (← field e "formattedMessage")}" - let sources ← field output "sources" - let sourceKeys ← objKeys sources - expect (sourceKeys == [logicalPath]) "unexpected compiler sources" - let sourceOut ← field sources logicalPath - requireKeys sourceOut ["ast", "id"] "source output" - let sourceId ← nat (← field sourceOut "id") - let ast ← field sourceOut "ast" - let ctx := { path := sourcePath, logicalPath, bytes := raw, sourceId } - needAt ctx ast ((← nodeKind ast) == "SourceUnit") "root must be SourceUnit" - validateNode ctx ast - needAt ctx ast ((← str (← field ast "absolutePath")) == logicalPath) - "source-unit path mismatch" - let ids ← collectAstIds ast - needAt ctx ast (ids.length == ids.eraseDups.length) "duplicate Solidity AST node ID" - let mut contracts : List Json := [] - for node in (← arr (← field ast "nodes")) do - match ← nodeKind node with - | "PragmaDirective" => - let literals ← arr (← field node "literals") - needAt ctx node (literals.size == 4 && (← str literals[0]!) == "solidity" && - (← str literals[1]!) == "^" && (← str literals[2]!) == "0.8" && - (← str literals[3]!) == ".33") "unsupported pragma" - | "ContractDefinition" => contracts := contracts ++ [node] - | "StructDefinition" => pure () - | "ErrorDefinition" => pure () - | _ => failAt ctx node "unsupported source declaration" - needAt ctx ast (!contracts.isEmpty) "exactly one concrete contract required" - let mut concrete : List Json := [] - for c in contracts do - unless (← bool (← field c "abstract")) do concrete := concrete ++ [c] - let target ← match targetName? with - | none => - needAt ctx ast (concrete.length == 1) "exactly one concrete contract required" - pure concrete.head! - | some name => - let some c := contracts.find? fun n => (field? n "name").bind (·.getStr?.toOption) == some name - | throwError "unknown contract {name}" - needAt ctx c (!(← bool (← field c "abstract")) && (← bool (← field c "fullyImplemented"))) - "target contract must be fully implemented" - pure c - let targetId ← nodeId target - let targetName ← str (← field target "name") - needAt ctx target ((← str (← field target "contractKind")) == "contract" && - !(← bool (← field target "abstract")) && (← bool (← field target "fullyImplemented")) && - (← arr (← field target "usedEvents")).isEmpty) - "inheritance/abstract contract unsupported" - let mut contractById : List (Nat × String × Json) := [] - let mut contractIds : List Nat := [] - for c in contracts do - needAt ctx c ((← str (← field c "contractKind")) == "contract") "unsupported contract kind" - needAt ctx c ((← str (← field c "canonicalName")) == (← str (← field c "name")) && - (← nat (← field c "scope")) == (← nodeId ast)) "contract identity mismatch" - let cid ← nodeId c - contractById := contractById ++ [(cid, ← str (← field c "name"), c)] - contractIds := contractIds ++ [cid] - for c in contracts do - for dep in (← arr (← field c "contractDependencies")) do - needAt ctx c (contractIds.contains (← nat dep)) "is referencing a contract from another file" - for b in (← arr (← field c "baseContracts")) do - let baseName ← field b "baseName" - let rid ← int (← field baseName "referencedDeclaration") - needAt ctx b (rid >= 0 && contractIds.contains rid.toNat) - "is referencing a contract from another file" - let linearized ← arr (← field target "linearizedBaseContracts") - needAt ctx target (linearized.size >= 1 && (← nat linearized[0]!) == targetId) - "invalid contract linearization" - let mut linearization : List Nat := [] - for idj in linearized do - let id ← nat idj - needAt ctx target (contractIds.contains id) "invalid contract linearization" - linearization := linearization ++ [id] - let exports ← field ast "exportedSymbols" - needAt ctx ast ((← objKeys exports).contains targetName) "exported symbol mismatch" - let exportedIds ← arr (← field exports targetName) - needAt ctx ast (exportedIds.any fun id => (id.getNat?.toOption == some targetId)) - "exported symbol mismatch" - let compilerContracts ← field output "contracts" - expect ((← objKeys compilerContracts) == [logicalPath]) "unexpected compiler contract sources" - let sourceContracts ← field compilerContracts logicalPath - expect ((← objKeys sourceContracts).contains targetName) "unexpected compiler contracts" - let contractOut ← field sourceContracts targetName - requireKeys contractOut ["storageLayout"] "contract output" - let layout ← field contractOut "storageLayout" - requireKeys layout ["storage", "types"] "storage layout" - let storage ← arr (← field layout "storage") - let layoutTypes ← field layout "types" - let mut fields : List FieldInfo := [] - let mut opaqueEntries : List OpaqueField := [] - let mut errors : List (Nat × String) := [] - let mut structs : List StructInfo := [] - let mut modifiers : List ModInfo := [] - let mut allFns : List FnInfo := [] - for node in (← arr (← field ast "nodes")) do - if (← nodeKind node) == "StructDefinition" then - let members ← parseStructMembers ctx node - structs := structs ++ [{ - id := ← nodeId node - name := ← identifier ctx node - canonicalName := ← str (← field node "canonicalName") - members }] - else if (← nodeKind node) == "ErrorDefinition" then - let ps ← arr (← field (← field node "parameters") "parameters") - needAt ctx node ps.isEmpty "only zero-argument custom errors" - errors := errors ++ [((← nodeId node), (← identifier ctx node))] - for cid in linearization do - let some (_, _, cnode) := contractById.find? (·.1 == cid) - | failAt ctx target "invalid contract linearization" - for node in (← arr (← field cnode "nodes")) do - if (← nodeKind node) == "StructDefinition" then - let members ← parseStructMembers ctx node - structs := structs ++ [{ - id := ← nodeId node - name := ← identifier ctx node - canonicalName := ← str (← field node "canonicalName") - members }] - for cid in linearization do - let some (_, cname, cnode) := contractById.find? (·.1 == cid) - | failAt ctx target "invalid contract linearization" - for node in (← arr (← field cnode "nodes")) do - match ← nodeKind node with - | "VariableDeclaration" => - needAt ctx node ((← bool (← field node "stateVariable")) && - !(← bool (← field node "constant")) && - (← str (← field node "mutability")) == "mutable" && - (field? node "value").all Json.isNull && - (← str (← field node "storageLocation")) == "default" && - (← nat (← field node "scope")) == cid) "initializer/constant/transient field unsupported" - let id ← nodeId node - let some entry := storage.find? fun e => (field? e "astId").bind (·.getNat?.toOption) == some id - | failAt ctx node "missing storage layout" - requireKeys entry ["astId", "contract", "label", "offset", "slot", "type"] "storage entry" - let label ← str (← field entry "label") - needAt ctx node ((← str (← field entry "contract")) == s!"{logicalPath}:{targetName}" && - (← str (← field node "name")) == label) "storage declaration mismatch" - needAt ctx node ((← nat (← field entry "offset")) == 0) "missing/packed layout" - let typ ← typeString node - let typeId ← str (← field entry "type") - let layoutType ← field layoutTypes typeId - let slotText ← str (← field entry "slot") - let some slot := slotText.toNat? | failAt ctx node "invalid storage slot" - let name ← identifier ctx node - if typ == "uint256" || typ == "mapping(address => uint256)" || typ == "address" then - let sty : Sol.StorageTy := - if typ == "mapping(address => uint256)" then .mapping - else if typ == "address" then .addr else .scalar - let expectedTypeKeys := - if sty == .mapping then ["encoding", "key", "label", "numberOfBytes", "value"] - else ["encoding", "label", "numberOfBytes"] - requireKeys layoutType expectedTypeKeys "storage type" - match sty with - | .scalar => - needAt ctx node ((← str (← field layoutType "numberOfBytes")) == "32" && - (← str (← field layoutType "encoding")) == "inplace" && - (← str (← field layoutType "label")) == "uint256") "bad scalar layout" - | .addr => - needAt ctx node ((← str (← field layoutType "numberOfBytes")) == "20" && - (← str (← field layoutType "encoding")) == "inplace" && - (← str (← field layoutType "label")) == "address") "bad scalar layout" - | .mapping => - needAt ctx node ((← str (← field layoutType "numberOfBytes")) == "32") "nonword layout" - let keyType ← field layoutTypes (← str (← field layoutType "key")) - let valueType ← field layoutTypes (← str (← field layoutType "value")) - requireKeys keyType ["encoding", "label", "numberOfBytes"] "mapping key type" - requireKeys valueType ["encoding", "label", "numberOfBytes"] "mapping value type" - needAt ctx node ((← str (← field layoutType "encoding")) == "mapping" && - (← str (← field keyType "encoding")) == "inplace" && - (← str (← field keyType "label")) == "address" && - (← str (← field keyType "numberOfBytes")) == "20" && - (← str (← field valueType "encoding")) == "inplace" && - (← str (← field valueType "label")) == "uint256" && - (← str (← field valueType "numberOfBytes")) == "32") "bad mapping layout" - let vis ← str (← field node "visibility") - let getter := if vis == "public" then some name else none - fields := fields ++ [FieldInfo.mk id name (name ++ "Slot") getter slot sty none none] - else if typ.startsWith "struct " then - let some typeName := field? node "typeName" | failAt ctx node "unsupported value type" - needAt ctx node ((← nodeKind typeName) == "UserDefinedTypeName") "unsupported value type" - let rid ← int (← field typeName "referencedDeclaration") - needAt ctx node (rid >= 0) "unresolved struct" - let some info := lookupStruct structs rid.toNat | failAt ctx node "using an undeclared struct" - requireKeys layoutType ["encoding", "label", "members", "numberOfBytes"] "storage type" - needAt ctx node ((← str (← field layoutType "encoding")) == "inplace") "bad scalar layout" - let vis ← str (← field node "visibility") - let mut i : Nat := 0 - for mem in info.members do - let sty : Sol.StorageTy := match mem.ty with - | .uint => .scalar - | .addr => .addr - | .pair _ => .scalar - let mname := name ++ "_" ++ mem.name - let getter := if vis == "public" && i == 0 then some name else none - fields := fields ++ [FieldInfo.mk id mname (mname ++ "Slot") getter (slot + i) sty (some info.id) (some i)] - i := i + 1 - else - let vis ← str (← field node "visibility") - needAt ctx node (vis != "public") "opaque public getter unsupported" - let typeLabel ← str (← field layoutType "label") - opaqueEntries := opaqueEntries ++ [OpaqueField.mk id label slot typeLabel] - | "ErrorDefinition" => - let ps ← arr (← field (← field node "parameters") "parameters") - needAt ctx node ps.isEmpty "only zero-argument custom errors" - errors := errors ++ [((← nodeId node), (← identifier ctx node))] - | "FunctionDefinition" => - needAt ctx node ((← nat (← field node "scope")) == cid) "function scope mismatch" - allFns := allFns ++ [← parseFnInfo ctx structs cid cname (cid == targetId) node] - | "ModifierDefinition" => - needAt ctx node (!(← bool (← field node "virtual"))) "unsupported modifier" - let ps ← arr (← field (← field node "parameters") "parameters") - needAt ctx node ps.isEmpty "modifier arguments" - let mname ← identifier ctx node - if modifiers.any (·.name == mname) then failAt ctx node "unsupported/reserved name" - modifiers := modifiers ++ [{ id := ← nodeId node, name := mname, contractId := cid, node }] - | "StructDefinition" => pure () - | _ => failAt ctx node "unsupported contract declaration" - let accounted := storage.all fun e => - (field? e "astId").bind (·.getNat?.toOption) |>.any fun id => - fields.any (·.id == id) || opaqueEntries.any (·.id == id) - needAt ctx target accounted "unaccounted layout field" - let names := fields.flatMap fun f => f.name :: f.getter.toList - needAt ctx target (names.length == names.eraseDups.length) "storage/generated name collision" - let slots := (fields.map (·.slot)) ++ (opaqueEntries.map (·.slot)) - needAt ctx target (slots.length == slots.eraseDups.length) "storage slot collision" - let mut usedErrorIds : List Nat := [] - for cid in linearization do - let some (_, _, cnode) := contractById.find? (·.1 == cid) | failAt ctx target "invalid contract linearization" - for errorId in (← arr (← field cnode "usedErrors")) do - usedErrorIds := usedErrorIds ++ [← nat errorId] - needAt ctx target (usedErrorIds.all fun id => errors.any (·.1 == id)) "custom error reference mismatch" - for f in allFns do - for g in allFns do - needAt ctx f.node (!(f.name == g.name && f.paramTys != g.paramTys)) - "function overloading by parameter type unsupported" - let functions := allFns.filter (·.implemented) - let sourceText := String.fromUTF8? raw |>.getD "" - let digest := sha256Hex (sourceText ++ outputText ++ importerText ++ solcVersionPin).toUTF8 - pure { - source := ctx, ast, targetName, targetId, linearization, contractById, - fields, opaqueEntries, errors, structs, modifiers, allFns, functions, digest - } - -private def uint := mkConst ``Verity.Core.Uint256 -private def address := mkConst ``Verity.Core.Address - -private def register (name : Name) (value : Expr) (type? : Option Expr := none) - (simp : Bool := true) : MetaM Unit := do - if (← getEnv).contains name then throwError "declaration collision: {name}" - let value ← instantiateMVars value - let type ← instantiateMVars (← type?.getDM (inferType value)) - if value.hasMVar || value.hasFVar || type.hasMVar || type.hasFVar then - throwError "unclosed imported declaration {name}" - addDecl (.defnDecl { name, levelParams := [], type, value, hints := .regular 0, safety := .safe }) - (forceExpose := true) - compileDecls #[name] (logErrors := false) - if simp then - let some ext ← getSimpExtension? `solidity_import - | throwError "solidity_import simp set is not registered" - ext.add (SimpEntry.toUnfold name) AttributeKind.global - -private def requireType (frontend : Frontend) (j : Json) (expected : String) : MetaM Unit := do - let actual ← typeString j - if expected.startsWith "struct " then - needAt frontend.source j (actual == expected || actual == expected ++ " memory" || - actual == expected ++ " storage ref") ("expected " ++ expected) - else - let identifier ← str (← field (← field j "typeDescriptions") "typeIdentifier") - let expectedIdentifier := match expected with - | "uint256" => "t_uint256" - | "address" => "t_address" - | "mapping(address => uint256)" => "t_mapping$_t_address_$_t_uint256_$" - | "msg" => "t_magic_message" - | "bool" => "t_bool" - | "tuple()" => "t_tuple$__$" - | _ => "" - needAt frontend.source j (actual == expected && identifier == expectedIdentifier) ("expected " ++ expected) - -private def requireUintish (frontend : Frontend) (j : Json) : MetaM Unit := do - let actual ← typeString j - if actual.startsWith "int_const " then pure () - else requireType frontend j "uint256" - -private def valueType : Sol.Ty → Expr - | .uint => uint - | .addr => address - | .pair _ => mkConst ``Sol.Pair - -private def typeName : Sol.Ty → String - | .uint => "uint256" - | .addr => "address" - | .pair _ => "struct" - -private def pairTypeNameOf (info : StructInfo) : String := - "struct " ++ info.canonicalName - -private def pairTypeName (frontend : Frontend) (id : Nat) : Option String := - (lookupStruct frontend.structs id).map pairTypeNameOf - -private def isPairTypeString (frontend : Frontend) (typ : String) : Bool := - frontend.structs.any (fun s => - s.members.map (·.ty) == [.uint, .addr] && - (typ == pairTypeNameOf s || typ == pairTypeNameOf s ++ " memory" || - typ == pairTypeNameOf s ++ " storage ref")) - -private def lookupStructByTypeString (frontend : Frontend) (typ : String) : Option StructInfo := - frontend.structs.find? (fun s => - s.members.map (·.ty) == [.uint, .addr] && - (typ == pairTypeNameOf s || typ == pairTypeNameOf s ++ " memory" || - typ == pairTypeNameOf s ++ " storage ref")) - -private def resultType : List Sol.Ty → Expr - | [] => mkConst ``Unit - | [t] => valueType t - | _ => mkConst ``Unit - -private def layoutOf (fields : List FieldInfo) : Sol.Layout := fields.map (·.sty) - -private def fieldIndex (fields : List FieldInfo) (id : Nat) : Option Nat := - go fields 0 -where - go : List FieldInfo → Nat → Option Nat - | [], _ => none - | f :: fs, i => if f.id == id then some i else go fs (i + 1) - -private def fieldIndexAt (fields : List FieldInfo) (pos : Nat) : Option Nat := - if pos < fields.length then some pos else none - -private def svarAt (frontend : Frontend) (L : Sol.Layout) (pos : Nat) (s : Sol.StorageTy) - (j : Json) : MetaM (Sol.SVar L s) := do - let some entry := Sol.SVar.ofIndex L pos | failAt frontend.source j "unresolved declaration id" - if h : entry.1 = s then pure (h ▸ entry.2) else failAt frontend.source j "storage type mismatch" - -private def svar (frontend : Frontend) (L : Sol.Layout) (id : Nat) (s : Sol.StorageTy) - (j : Json) : MetaM (Sol.SVar L s) := do - let some i := fieldIndex frontend.fields id | failAt frontend.source j "unresolved declaration id" - svarAt frontend L i s j - -private structure Scope (Γ : Sol.Ctx) where - vars : List (Nat × Σ t, Sol.Var Γ t) - -private def Scope.push (sc : Scope Γ) (id : Nat) (t : Sol.Ty) : Scope (t :: Γ) := - ⟨(id, ⟨t, .here⟩) :: sc.vars.map fun v => (v.1, ⟨v.2.1, .there v.2.2⟩)⟩ - -private def Scope.find? (sc : Scope Γ) (id : Nat) : Option (Σ t, Sol.Var Γ t) := - (sc.vars.find? fun v => v.1 == id).map (·.2) - -private def varHandles : (Γ : Sol.Ctx) → List (Σ t, Sol.Var Γ t) - | [] => [] - | t :: ts => ⟨t, .here⟩ :: (varHandles ts).map fun v => ⟨v.1, .there v.2⟩ - -private def scopeOf (params : List (Nat × Sol.Ty × String)) (Γ : Sol.Ctx) : Scope Γ := - ⟨(varHandles Γ).zip params.reverse |>.map fun entry => (entry.2.1, entry.1)⟩ - -private def slotsTerm : (handles : List (Sol.StorageTy × Expr)) → MetaM Expr - | [] => pure (mkConst ``Sol.Slots.nil) - | (st, h) :: rest => do - let restE ← slotsTerm rest - let tail : Sol.Layout := rest.map (·.1) - mkAppOptM ``Sol.Slots.cons #[some (toExpr tail), some (toExpr st), some h, some restE] - -private def envTerm : (xs : List (Expr × Sol.Ty)) → MetaM Expr - | [] => pure (mkConst ``Sol.Env.nil) - | (x, t) :: rest => do - let restE ← envTerm rest - let tail : Sol.Ctx := rest.map (·.2) - mkAppOptM ``Sol.Env.cons #[some (toExpr tail), some (toExpr t), some x, some restE] - -private partial def withParams (ps : List (Nat × Sol.Ty × String)) (acc : List (Expr × Sol.Ty)) - (k : List (Expr × Sol.Ty) → MetaM Expr) : MetaM Expr := do - match ps with - | [] => k acc - | p :: rest => - withLocalDeclD (Name.mkSimple p.2.2) (valueType p.2.1) fun x => do - mkLambdaFVars #[x] (← withParams rest ((x, p.2.1) :: acc) k) - -private def familyOf (frontend : Frontend) (name : String) (paramTys : List Sol.Ty) : List FnInfo := - frontend.allFns.filter fun f => f.name == name && f.paramTys == paramTys - -private def mostDerived (frontend : Frontend) (name : String) (paramTys : List Sol.Ty) : Option FnInfo := - Id.run do - for cid in frontend.linearization do - if let some f := (familyOf frontend name paramTys).find? (fun g => g.contractId == cid && g.implemented) then - return some f - none - -private def superTarget (frontend : Frontend) (definingId : Nat) (name : String) - (paramTys : List Sol.Ty) : Option FnInfo := - Id.run do - let mut seen := false - for cid in frontend.linearization do - if cid == definingId then seen := true - else if seen then - if let some f := (familyOf frontend name paramTys).find? (fun g => g.contractId == cid && g.implemented) then - return some f - none - -private def lookupFn (frontend : Frontend) (id : Nat) : Option FnInfo := - frontend.allFns.find? (·.id == id) - -private def isErrorRef (frontend : Frontend) (id : Nat) : Bool := - frontend.errors.any (·.1 == id) - -private def resolveCall (frontend : Frontend) (_current : FnInfo) (definingId : Nat) - (call : Json) : MetaM FnInfo := do - let callee ← field call "expression" - match ← nodeKind callee with - | "Identifier" => - let rid ← int (← field callee "referencedDeclaration") - needAt frontend.source callee (rid >= 0) "unresolved builtin identifier" - let id := rid.toNat - if isErrorRef frontend id then failAt frontend.source call "unsupported revert" - let some static := lookupFn frontend id | failAt frontend.source call "unresolved function reference" - needAt frontend.source call ((familyOf frontend static.name static.paramTys).any (·.id == id)) - "virtual dispatch disagrees with AST" - if static.visibility == "private" || !static.virtual then - pure static - else - let some resolved := mostDerived frontend static.name static.paramTys - | failAt frontend.source call "unimplemented virtual" - pure resolved - | "MemberAccess" => - let base ← field callee "expression" - needAt frontend.source callee ((← nodeKind base) == "Identifier" && - (← str (← field base "name")) == "super" && - (← int (← field base "referencedDeclaration")) == -25) - "only super.f() member calls supported" - let rid ← int (← field callee "referencedDeclaration") - needAt frontend.source callee (rid >= 0) "unresolved super target" - let some static := lookupFn frontend rid.toNat - | failAt frontend.source call "unresolved function reference" - -- solc's AST `referencedDeclaration` on `super.f` is the next override in - -- the *defining* contract's linearization. Runtime (and this importer) - -- use the *target* contract's C3 (`linearizedBaseContracts`), which - -- disagrees on diamonds. Follow `superTarget`; only require the AST id - -- to name a member of the same virtual family. - needAt frontend.source callee - ((familyOf frontend static.name static.paramTys).any (·.id == rid.toNat)) - "super dispatch family mismatch" - let some resolved := superTarget frontend definingId static.name static.paramTys - | failAt frontend.source call "unresolved super target" - pure resolved - | _ => failAt frontend.source call "unsupported call surface" - -private partial def collectCallIds (frontend : Frontend) (current : FnInfo) (definingId : Nat) - (j : Json) : MetaM (List Nat) := do - match j with - | .obj o => - let walkChildren : MetaM (List Nat) := do - let rest ← o.toList.mapM fun (_, v) => collectCallIds frontend current definingId v - pure rest.flatten - match field? j "nodeType" with - | some nt => - if (← str nt) == "FunctionCall" then - if (field? j "kind").bind (·.getStr?.toOption) == some "structConstructorCall" then - walkChildren - else do - let callee ← field j "expression" - let skip ← match ← nodeKind callee with - | "Identifier" => - let rid ← int (← field callee "referencedDeclaration") - pure (rid >= 0 && isErrorRef frontend rid.toNat) - | _ => pure false - let restIds ← walkChildren - if skip then pure restIds - else - let resolved ← resolveCall frontend current definingId j - pure (resolved.id :: restIds) - else walkChildren - | none => walkChildren - | .arr xs => - let rest ← xs.toList.mapM (collectCallIds frontend current definingId) - pure rest.flatten - | _ => pure [] - -private def lookupMod (frontend : Frontend) (id : Nat) : Option ModInfo := - frontend.modifiers.find? (·.id == id) - -private partial def countPlaceholders (j : Json) : Nat := - match j with - | .obj o => - let here := if (j.getObjVal? "nodeType").toOption == some (Json.str "PlaceholderStatement") then 1 else 0 - o.foldl (fun acc _ v => acc + countPlaceholders v) here - | .arr xs => xs.foldl (fun acc v => acc + countPlaceholders v) 0 - | _ => 0 - -private def splitModifierBody (stmts : List Json) : MetaM (List Json × List Json) := do - let mut pre : List Json := [] - let mut post : List Json := [] - let mut seen := false - for s in stmts do - if (← nodeKind s) == "PlaceholderStatement" then - if seen then throwError "modifier with two placeholders" - seen := true - else if seen then post := post ++ [s] - else pre := pre ++ [s] - unless seen do throwError "modifier missing placeholder" - pure (pre, post) - -private def fnModifierInvocations (fn : FnInfo) : MetaM (Array Json) := do - arr (← field fn.node "modifiers") - -private def topoSort (frontend : Frontend) : MetaM (List FnInfo) := do - let mut deps : List (Nat × List Nat) := [] - for f in frontend.functions do - let body := field? f.node "body" |>.getD Json.null - let mut callees := (← collectCallIds frontend f f.contractId body).eraseDups - for inv in (← fnModifierInvocations f) do - let nameNode ← field inv "modifierName" - let rid ← int (← field nameNode "referencedDeclaration") - needAt frontend.source inv (rid >= 0) "unresolved modifier" - let some m := lookupMod frontend rid.toNat | failAt frontend.source inv "unresolved modifier" - let mbody := field? m.node "body" |>.getD Json.null - callees := (callees ++ (← collectCallIds frontend f m.contractId mbody)).eraseDups - deps := deps ++ [(f.id, callees)] - let mut remaining := frontend.functions - let mut order : List FnInfo := [] - for _ in List.range (frontend.functions.length + 1) do - if remaining.isEmpty then return order - match remaining.find? fun f => - ((deps.find? (·.1 == f.id)).map (·.2) |>.getD []).all fun c => - order.any (·.id == c) with - | some next => - order := order ++ [next] - remaining := remaining.filter (fun g => g.id != next.id) - | none => - failAt frontend.source frontend.ast - s!"direct and mutual recursion unsupported ({String.intercalate ", " (remaining.map (·.leanIdent))})" - failAt frontend.source frontend.ast "direct and mutual recursion unsupported" - -private structure ParseCtx where - frontend : Frontend - L : Sol.Layout - F : Sol.Fns - registered : List FnInfo - current : FnInfo - superFrom : Nat - -private def indexOfFn : List FnInfo → Nat → Nat → Option Nat - | [], _, _ => none - | x :: xs, id, i => if x.id == id then some i else indexOfFn xs id (i + 1) - -private def fvarOf (ctx : ParseCtx) (resolved : FnInfo) (j : Json) : MetaM (Σ σ, Sol.FVar ctx.F σ) := do - let some idx := indexOfFn ctx.registered resolved.id 0 - | failAt ctx.frontend.source j "direct and mutual recursion unsupported" - let some entry := Sol.FVar.ofIndex ctx.F idx - | failAt ctx.frontend.source j "unresolved function reference" - unless entry.1 == resolved.sig do failAt ctx.frontend.source j "function signature mismatch" - pure entry - -mutual -private partial def loadPair (ctx : ParseCtx) {Γ : Sol.Ctx} (id : Nat) (sid : Nat) (j : Json) : - MetaM (Sol.Expr ctx.L ctx.F Γ (.pair sid)) := do - let members := (List.range ctx.frontend.fields.length).zip ctx.frontend.fields |>.filter (fun p => p.2.id == id) - match members with - | (p0, m0) :: (p1, m1) :: [] => - needAt ctx.frontend.source j (m0.sty == .scalar && m1.sty == .addr) "storage type mismatch" - let a ← svarAt ctx.frontend ctx.L p0 .scalar j - let b ← svarAt ctx.frontend ctx.L p1 .addr j - pure (.pair (.load a) (.loadAddr b)) - | _ => failAt ctx.frontend.source j "storage type mismatch" - -private partial def parseExpr (ctx : ParseCtx) {Γ : Sol.Ctx} (sc : Scope Γ) (t : Sol.Ty) - (j : Json) : MetaM (Sol.Expr ctx.L ctx.F Γ t) := do - match ← nodeKind j with - | "Identifier" => - let rid ← int (← field j "referencedDeclaration") - if rid < 0 then failAt ctx.frontend.source j "unresolved builtin identifier" - let id := rid.toNat - if let some v := sc.find? id then - match v.1 with - | .pair sid => - let some expected := pairTypeName ctx.frontend sid | failAt ctx.frontend.source j "unsupported value type" - requireType ctx.frontend j expected - | _ => requireType ctx.frontend j (typeName v.1) - if h : v.1 = t then pure (.var (h ▸ v.2)) else failAt ctx.frontend.source j "local type mismatch" - else if ctx.frontend.fields.any (fun f => f.id == id && f.structId.isSome) then - match t with - | .pair sid => - let some expected := pairTypeName ctx.frontend sid | failAt ctx.frontend.source j "unsupported value type" - requireType ctx.frontend j expected - loadPair ctx id sid j - | _ => failAt ctx.frontend.source j "storage type mismatch" - else if let some info := ctx.frontend.fields.find? (·.id == id) then - match info.sty, t with - | .scalar, .uint => - requireType ctx.frontend j "uint256" - pure (.load (← svar ctx.frontend ctx.L id .scalar j)) - | .addr, .addr => - requireType ctx.frontend j "address" - pure (.loadAddr (← svar ctx.frontend ctx.L id .addr j)) - | .mapping, _ => failAt ctx.frontend.source j "mapping requires index access" - | _, _ => failAt ctx.frontend.source j "storage type mismatch" - else if ctx.frontend.opaqueEntries.any (·.id == id) then - failAt ctx.frontend.source j "opaque field" - else failAt ctx.frontend.source j "unresolved declaration reference" - | "MemberAccess" => - let base ← field j "expression" - let member ← str (← field j "memberName") - if member == "sender" && (← nodeKind base) == "Identifier" && - (← str (← field base "name")) == "msg" && - (← int (← field base "referencedDeclaration")) == -15 && - (field? j "referencedDeclaration").all fun v => v.isNull then - requireType ctx.frontend base "msg" - requireType ctx.frontend j "address" - match t with - | .addr => pure .sender - | .uint => failAt ctx.frontend.source j "address expression in uint256 position" - | .pair _ => failAt ctx.frontend.source j "address expression in uint256 position" - else - unless isPairTypeString ctx.frontend (← typeString base) do - failAt ctx.frontend.source j "only builtin msg.sender supported" - let some info := lookupStructByTypeString ctx.frontend (← typeString base) - | failAt ctx.frontend.source j "using an undeclared struct" - let some idx := info.members.findIdx? (·.name == member) - | failAt ctx.frontend.source j "unresolved declaration reference" - let baseE ← parseExpr ctx sc (.pair info.id) base - match idx, t with - | 0, .uint => - requireType ctx.frontend j "uint256" - pure (.fst baseE) - | 1, .addr => - requireType ctx.frontend j "address" - pure (.snd baseE) - | _, _ => failAt ctx.frontend.source j "local type mismatch" - | "IndexAccess" => - let base ← field j "baseExpression" - needAt ctx.frontend.source j ((← nodeKind base) == "Identifier") "unsupported index base" - let rid ← int (← field base "referencedDeclaration") - if rid < 0 then failAt ctx.frontend.source j "unsupported index base" - if ctx.frontend.opaqueEntries.any (·.id == rid.toNat) then failAt ctx.frontend.source j "opaque field" - let some info := ctx.frontend.fields.find? (·.id == rid.toNat) - | failAt ctx.frontend.source j "unsupported index base" - needAt ctx.frontend.source j (info.sty == .mapping) "unsupported index base" - requireType ctx.frontend base "mapping(address => uint256)" - let index ← field j "indexExpression" - requireType ctx.frontend index "address" - requireType ctx.frontend j "uint256" - let key ← parseExpr ctx sc .addr index - match t with - | .uint => pure (.index (← svar ctx.frontend ctx.L info.id .mapping j) key) - | .addr => failAt ctx.frontend.source j "uint256 expression in address position" - | .pair _ => failAt ctx.frontend.source j "uint256 expression in address position" - | "Literal" => - let value ← str (← field j "value") - let some n := value.toNat? | failAt ctx.frontend.source j "unsupported literal" - needAt ctx.frontend.source j ((← str (← field j "kind")) == "number" && - (field? j "subdenomination").all Json.isNull && n < 2^256) "unsupported literal" - needAt ctx.frontend.source j ((← typeString j).startsWith "int_const ") "unsupported literal type" - match t with - | .uint => pure (.lit n) - | .addr => failAt ctx.frontend.source j "uint256 expression in address position" - | .pair _ => failAt ctx.frontend.source j "uint256 expression in address position" - | "BinaryOperation" => - let op ← str (← field j "operator") - needAt ctx.frontend.source j (op == "+" || op == "-") "unsupported binary operation/types" - requireType ctx.frontend j "uint256" - let left ← field j "leftExpression" - let right ← field j "rightExpression" - requireType ctx.frontend left "uint256"; requireType ctx.frontend right "uint256" - let a ← parseExpr ctx sc .uint left - let b ← parseExpr ctx sc .uint right - let aop : Sol.ArithOp := if op == "+" then .add else .sub - match t with - | .uint => pure (.arith aop a b) - | .addr => failAt ctx.frontend.source j "uint256 expression in address position" - | .pair _ => failAt ctx.frontend.source j "uint256 expression in address position" - | "FunctionCall" => - if (← str (← field j "kind")) == "structConstructorCall" then - let some info := lookupStructByTypeString ctx.frontend (← typeString j) - | failAt ctx.frontend.source j "using an undeclared struct" - needAt ctx.frontend.source j (t == .pair info.id) "unsupported value type" - requireType ctx.frontend j (pairTypeNameOf info) - let argsJ ← arr (← field j "arguments") - let names ← arr (← field j "names") - needAt ctx.frontend.source j (argsJ.size == 2) "argument count mismatch" - let amountName := - match info.members with - | m0 :: _ => m0.name - | [] => "" - let whoName := - match info.members with - | _ :: m1 :: _ => m1.name - | _ => "" - let e : Sol.Expr ctx.L ctx.F Γ (.pair info.id) ← - if names.isEmpty then - let a ← parseExpr ctx sc .uint argsJ[0]! - let b ← parseExpr ctx sc .addr argsJ[1]! - pure (.pair a b) - else do - needAt ctx.frontend.source j (names.size == 2) "argument count mismatch" - let some iAmt := names.findIdx? (fun n => n.getStr?.toOption == some amountName) - | failAt ctx.frontend.source j "unresolved declaration reference" - let some iWho := names.findIdx? (fun n => n.getStr?.toOption == some whoName) - | failAt ctx.frontend.source j "unresolved declaration reference" - needAt ctx.frontend.source j (iAmt != iWho) "unresolved declaration reference" - if iAmt == 0 && iWho == 1 then - let a ← parseExpr ctx sc .uint argsJ[0]! - let b ← parseExpr ctx sc .addr argsJ[1]! - pure (.pair a b) - else if iAmt == 1 && iWho == 0 then - let b ← parseExpr ctx sc .addr argsJ[0]! - let a ← parseExpr ctx sc .uint argsJ[1]! - pure (.pairRev b a) - else failAt ctx.frontend.source j "unresolved declaration reference" - if h : t = .pair info.id then pure (h ▸ e) else failAt ctx.frontend.source j "local type mismatch" - else do - let resolved ← resolveCall ctx.frontend ctx.current ctx.superFrom j - needAt ctx.frontend.source j resolved.viewOrPure - "effectful internal call in expression position" - needAt ctx.frontend.source j (resolved.rets == [t]) "function does not return the expected type" - let ⟨σ, fv⟩ ← fvarOf ctx resolved j - let argsJ := (← arr (← field j "arguments")).toList - let args ← parseArgs ctx sc resolved.paramTys argsJ j - if h : t = .uint then - requireType ctx.frontend j "uint256" - if hσ : σ = ⟨resolved.paramTys.reverse, [.uint]⟩ then - let e : Sol.Expr ctx.L ctx.F Γ .uint := .call (hσ ▸ fv) args - pure (h ▸ e) - else failAt ctx.frontend.source j "function signature mismatch" - else if h : t = .addr then - requireType ctx.frontend j "address" - if hσ : σ = ⟨resolved.paramTys.reverse, [.addr]⟩ then - let e : Sol.Expr ctx.L ctx.F Γ .addr := .call (hσ ▸ fv) args - pure (h ▸ e) - else failAt ctx.frontend.source j "function signature mismatch" - else - match t with - | .pair sid => - let some expected := pairTypeName ctx.frontend sid | failAt ctx.frontend.source j "unsupported value type" - requireType ctx.frontend j expected - if hσ : σ = ⟨resolved.paramTys.reverse, [.pair sid]⟩ then - let e : Sol.Expr ctx.L ctx.F Γ (.pair sid) := .call (hσ ▸ fv) args - if h : t = .pair sid then pure (h ▸ e) - else failAt ctx.frontend.source j "function signature mismatch" - else failAt ctx.frontend.source j "function signature mismatch" - | _ => failAt ctx.frontend.source j "function signature mismatch" - | _ => failAt ctx.frontend.source j "unsupported expression" - -private partial def parseArgs (ctx : ParseCtx) {Γ : Sol.Ctx} (sc : Scope Γ) (ts : List Sol.Ty) - (js : List Json) (origin : Json) : MetaM (Sol.Args ctx.L ctx.F Γ ts) := do - match ts, js with - | [], [] => pure .nil - | t :: ts, j :: js => - let e ← parseExpr ctx sc t j - let rest ← parseArgs ctx sc ts js origin - pure (.cons e rest) - | _, _ => failAt ctx.frontend.source origin "argument count mismatch" - -private partial def parseLValue (ctx : ParseCtx) {Γ : Sol.Ctx} (sc : Scope Γ) (j : Json) : - MetaM (Sol.LVal ctx.L ctx.F Γ) := do - match ← nodeKind j with - | "Identifier" => - let rid ← int (← field j "referencedDeclaration") - if rid < 0 then failAt ctx.frontend.source j "unsupported storage lvalue" - if ctx.frontend.opaqueEntries.any (·.id == rid.toNat) then failAt ctx.frontend.source j "opaque field" - let some info := ctx.frontend.fields.find? (·.id == rid.toNat) - | failAt ctx.frontend.source j "unsupported storage lvalue" - needAt ctx.frontend.source j (info.sty == .scalar) "mapping requires index access" - requireType ctx.frontend j "uint256" - pure (.scalar (← svar ctx.frontend ctx.L info.id .scalar j)) - | "IndexAccess" => - let base ← field j "baseExpression" - needAt ctx.frontend.source j ((← nodeKind base) == "Identifier") "unsupported index base" - let rid ← int (← field base "referencedDeclaration") - if rid < 0 then failAt ctx.frontend.source j "unsupported index base" - if ctx.frontend.opaqueEntries.any (·.id == rid.toNat) then failAt ctx.frontend.source j "opaque field" - let some info := ctx.frontend.fields.find? (·.id == rid.toNat) - | failAt ctx.frontend.source j "unsupported index base" - needAt ctx.frontend.source j (info.sty == .mapping) "unsupported index base" - requireType ctx.frontend base "mapping(address => uint256)" - let index ← field j "indexExpression" - requireType ctx.frontend index "address" - requireType ctx.frontend j "uint256" - let key ← parseExpr ctx sc .addr index - pure (.mapping (← svar ctx.frontend ctx.L info.id .mapping j) key) - | _ => failAt ctx.frontend.source j "only storage assignment supported" - -private partial def assignStructMembers (ctx : ParseCtx) {Γ : Sol.Ctx} - (id : Nat) (sid : Nat) (rhs : Sol.Expr ctx.L ctx.F Γ (.pair sid)) - (rest : Sol.Stmt ctx.L ctx.F Γ r) - (j : Json) : MetaM (Sol.Stmt ctx.L ctx.F Γ r) := do - let members := (List.range ctx.frontend.fields.length).zip ctx.frontend.fields |>.filter (fun p => p.2.id == id) - match members with - | [(p0, m0), (p1, m1)] => - needAt ctx.frontend.source j (m0.sty == .scalar && m1.sty == .addr) "storage type mismatch" - let a ← svarAt ctx.frontend ctx.L p0 .scalar j - let b ← svarAt ctx.frontend ctx.L p1 .addr j - let shifted := rest.shift (.pair sid) 0 - let inner : Sol.Stmt ctx.L ctx.F ((.pair sid) :: Γ) r := - .assign (.scalar a) .set (.fst (.var .here)) - (.assignAddr b (.snd (.var .here)) shifted) - pure (.local_ rhs inner) - | _ => failAt ctx.frontend.source j "storage type mismatch" - -private partial def parseStmts (ctx : ParseCtx) {Γ : Sol.Ctx} (sc : Scope Γ) (r : Sol.Ret) - (namedReturn : Option Nat) (nodes : List Json) : MetaM (Sol.Stmt ctx.L ctx.F Γ r) := do - match nodes with - | [] => - match r with - | [] => pure .done - | [t] => - match namedReturn with - | some id => - let some v := sc.find? id | throwError "missing named return" - if h : v.1 = t then pure (.ret (.var (h ▸ v.2))) - else throwError "named return type mismatch" - | none => throwError "missing terminal return" - | _ => throwError "missing terminal return" - | node :: rest => - match ← nodeKind node with - | "PlaceholderStatement" => failAt ctx.frontend.source node "modifier with two placeholders" - | "ExpressionStatement" => - let expr ← field node "expression" - match ← nodeKind expr with - | "Assignment" => - let op ← str (← field expr "operator") - let lhs ← field expr "leftHandSide" - if (← nodeKind lhs) == "Identifier" then - let rid ← int (← field lhs "referencedDeclaration") - if rid >= 0 && ctx.frontend.opaqueEntries.any (·.id == rid.toNat) then - failAt ctx.frontend.source node "opaque field" - if rid >= 0 then - if ctx.frontend.fields.any (fun f => f.id == rid.toNat && f.structId.isSome) then - needAt ctx.frontend.source expr (op == "=") "unsupported assignment" - let some info := lookupStructByTypeString ctx.frontend (← typeString lhs) - | failAt ctx.frontend.source expr "using an undeclared struct" - let rhs ← parseExpr ctx sc (.pair info.id) (← field expr "rightHandSide") - let restS ← parseStmts ctx sc r namedReturn rest - return (← assignStructMembers ctx rid.toNat info.id rhs restS lhs) - if let some info := ctx.frontend.fields.find? (·.id == rid.toNat) then - if info.sty == .addr then - needAt ctx.frontend.source expr (op == "=") "unsupported assignment" - requireType ctx.frontend expr "address" - let rhs ← parseExpr ctx sc .addr (← field expr "rightHandSide") - let sv ← svar ctx.frontend ctx.L info.id .addr lhs - return .assignAddr sv rhs (← parseStmts ctx sc r namedReturn rest) - requireType ctx.frontend expr "uint256" - needAt ctx.frontend.source expr (op == "=" || op == "+=" || op == "-=") "unsupported assignment" - let lv ← parseLValue ctx sc lhs - let rhs ← parseExpr ctx sc .uint (← field expr "rightHandSide") - let aop : Sol.AssignOp := if op == "=" then .set else if op == "+=" then .add else .sub - pure (.assign lv aop rhs (← parseStmts ctx sc r namedReturn rest)) - | "FunctionCall" => - requireType ctx.frontend expr "tuple()" - let resolved ← resolveCall ctx.frontend ctx.current ctx.superFrom expr - needAt ctx.frontend.source expr resolved.rets.isEmpty "function does not return the expected type" - let ⟨σ, fv⟩ ← fvarOf ctx resolved expr - let argsJ := (← arr (← field expr "arguments")).toList - let args ← parseArgs ctx sc resolved.paramTys argsJ expr - let restS ← parseStmts ctx sc r namedReturn rest - if h : σ = ⟨resolved.paramTys.reverse, []⟩ then - pure (.callStmt (h ▸ fv) args restS) - else failAt ctx.frontend.source expr "function signature mismatch" - | _ => failAt ctx.frontend.source node "unsupported expression statement" - | "VariableDeclarationStatement" => - let declarations ← arr (← field node "declarations") - needAt ctx.frontend.source node (declarations.size == 1) "unsupported locals" - let declaration := declarations[0]! - needAt ctx.frontend.source declaration (!(← bool (← field declaration "stateVariable")) && - !(← bool (← field declaration "constant")) && - (← str (← field declaration "mutability")) == "mutable" && - (← str (← field declaration "storageLocation")) == "default" && - (← str (← field declaration "visibility")) == "internal") "unsupported local declaration" - requireType ctx.frontend declaration "uint256" - let id ← nodeId declaration - let _ ← identifier ctx.frontend.source declaration - let value ← parseExpr ctx sc .uint (← field node "initialValue") - pure (.local_ value (← parseStmts ctx (sc.push id .uint) r namedReturn rest)) - | "IfStatement" => - let falseBody := field? node "falseBody" - let trueBody ← field node "trueBody" - let revertNode ← match ← nodeKind trueBody with - | "Block" => - let statements ← arr (← field trueBody "statements") - needAt ctx.frontend.source node (statements.size == 1 && - (← nodeKind statements[0]!) == "RevertStatement") "only if/revert guard supported" - pure statements[0]! - | "RevertStatement" => pure trueBody - | _ => failAt ctx.frontend.source node "only if/revert guard supported" - needAt ctx.frontend.source node (falseBody.all Json.isNull) "only if/revert guard supported" - let call ← field revertNode "errorCall" - let callee ← field call "expression" - let rid ← int (← field callee "referencedDeclaration") - let some errorName := if rid < 0 then none else ctx.frontend.errors.lookup rid.toNat - | failAt ctx.frontend.source node "unsupported revert" - needAt ctx.frontend.source node ((← nodeKind callee) == "Identifier" && - (← arr (← field call "arguments")).isEmpty) "unsupported revert" - let condition ← field node "condition" - needAt ctx.frontend.source condition ((← nodeKind condition) == "BinaryOperation") - "only uint256 comparison guard supported" - requireType ctx.frontend condition "bool" - let op ← str (← field condition "operator") - let left ← field condition "leftExpression" - let right ← field condition "rightExpression" - if op == "<" then - let commonType ← field condition "commonType" - needAt ctx.frontend.source condition ((← str (← field commonType "typeString")) == "uint256" && - (← str (← field commonType "typeIdentifier")) == "t_uint256") "unsupported comparison type" - requireUintish ctx.frontend left; requireUintish ctx.frontend right - let a ← parseExpr ctx sc .uint left - let b ← parseExpr ctx sc .uint right - pure (.guard a b (errorName ++ "()") (← parseStmts ctx sc r namedReturn rest)) - else if op == "!=" then - let commonType ← field condition "commonType" - needAt ctx.frontend.source condition ((← str (← field commonType "typeString")) == "address") - "unsupported comparison type" - requireType ctx.frontend left "address"; requireType ctx.frontend right "address" - let a ← parseExpr ctx sc .addr left - let b ← parseExpr ctx sc .addr right - pure (.guardAddrNe a b (errorName ++ "()") (← parseStmts ctx sc r namedReturn rest)) - else failAt ctx.frontend.source condition "only uint256 comparison guard supported" - | "Return" => - needAt ctx.frontend.source node rest.isEmpty "only terminal scalar return" - match r with - | [t] => - match field? node "expression" with - | some expr => - needAt ctx.frontend.source node (!expr.isNull) "only terminal scalar return" - let e ← parseExpr ctx sc t expr - pure (.ret e) - | none => failAt ctx.frontend.source node "only terminal scalar return" - | [] => - needAt ctx.frontend.source node ((field? node "expression").all fun e => e.isNull) - "only terminal scalar return" - pure .done - | _ => failAt ctx.frontend.source node "only terminal scalar return" - | _ => failAt ctx.frontend.source node "unsupported statement" -end - -private def checkCollisions (ns : Name) (frontend : Frontend) (order : List FnInfo) : MetaM Unit := do - let mut names := #[ - ns ++ `sourceDigest, ns ++ `Storage, ns ++ `view, ns ++ `step, - ns ++ `Storage ++ `mk, ns ++ `opaqueFields, ns ++ `registeredSources] - for f in frontend.fields do - names := names.push (ns ++ Name.mkSimple f.name) - names := names.push (ns ++ `Storage ++ Name.mkSimple f.var) - if let some getter := f.getter then names := names.push (ns ++ Name.mkSimple getter) - for fn in order do - names := names.push (ns ++ Name.mkSimple fn.leanIdent) - for i in [:names.size] do - if (← getEnv).contains names[i]! || (names.extract 0 i).contains names[i]! then - throwError "declaration collision: {names[i]!}" - -private def storageStructure (alias : Name) (frontend : Frontend) : CommandElabM Syntax := do - let declId := mkIdent (alias ++ `Storage) - let binders : TSyntaxArray ``Parser.Command.structExplicitBinder ← - frontend.fields.toArray.mapM fun f => do - let fieldId := mkIdent (Name.mkSimple f.var) - let ty : Term ← match f.sty with - | .mapping => `(Verity.Address → Verity.Uint256) - | .addr => `(Verity.Address) - | .scalar => `(Verity.Uint256) - `(Parser.Command.structExplicitBinder| ($fieldId:ident : $ty)) - `(command| structure $declId where $binders:structExplicitBinder*) - -private def mkEntryDisjunct (s s' : Expr) (name : Name) (binders : List (Name × Expr)) : MetaM Expr := - go binders #[] -where - go : List (Name × Expr) → Array Expr → MetaM Expr - | [], xs => do - let run ← mkAppM ``Verity.Contract.run #[mkAppN (mkConst name) xs, s] - let snd ← mkAppM ``Verity.ContractResult.snd #[run] - mkEq s' snd - | (n, dom) :: rest, xs => - withLocalDeclD n dom fun x => do - let inner ← go rest (xs.push x) - mkAppOptM ``Exists #[some dom, some (← mkLambdaFVars #[x] inner)] - -private def envApply (σ : Sol.Sig) (fn : Expr) : MetaM Expr := do - let envTy ← mkAppM ``Sol.Env #[toExpr σ.params] - withLocalDeclD `env envTy fun env => do - let mut app := fn - let n := σ.params.length - for i in [:n] do - let dbIndex := n - 1 - i - let some v := Sol.Var.ofIndex σ.params dbIndex | throwError "envApply: missing binder" - let getter ← mkAppOptM ``Sol.Env.get - #[some (toExpr σ.params), some (toExpr v.1), some env, some (toExpr v.2)] - app := mkApp app getter - mkLambdaFVars #[env] app - -private def fnsTerm : (handles : List (Sol.Sig × Expr)) → MetaM Expr - | [] => pure (mkConst ``Sol.FnEnv.nil) - | (σ, h) :: rest => do - let restE ← fnsTerm rest - let apply ← envApply σ h - let envTy ← mkAppM ``Sol.Env #[toExpr σ.params] - let retTy ← mkAppM ``Sol.Ret.denote #[toExpr σ.rets] - let contractTy ← mkAppM ``Verity.Contract #[retTy] - let applyTy ← mkArrow envTy contractTy - let apply ← mkExpectedTypeHint apply applyTy - let tail : Sol.Fns := rest.map (·.1) - -- `FnEnv.cons` implicits are `{F : Fns}` then `{σ : Sig}`. - pure (mkAppN (mkConst ``Sol.FnEnv.cons) #[toExpr tail, toExpr σ, apply, restE]) - -private partial def wrapModifierParts (ctx : ParseCtx) {Γ : Sol.Ctx} (sc : Scope Γ) - (pre post : List Json) (inner : Sol.Stmt ctx.L ctx.F Γ r) : - MetaM (Sol.Stmt ctx.L ctx.F Γ r) := do - match pre with - | [] => - let postS ← parseStmts ctx sc [] none post - pure (.block inner postS) - | node :: restPre => - if (← nodeKind node) == "Return" then - failAt ctx.frontend.source node "modifier prelude return skips function body" - if (← nodeKind node) == "VariableDeclarationStatement" then - let declarations ← arr (← field node "declarations") - needAt ctx.frontend.source node (declarations.size == 1) "unsupported locals" - let declaration := declarations[0]! - needAt ctx.frontend.source declaration (!(← bool (← field declaration "stateVariable")) && - !(← bool (← field declaration "constant")) && - (← str (← field declaration "mutability")) == "mutable" && - (← str (← field declaration "storageLocation")) == "default" && - (← str (← field declaration "visibility")) == "internal") "unsupported local declaration" - requireType ctx.frontend declaration "uint256" - let id ← nodeId declaration - let _ ← identifier ctx.frontend.source declaration - let value ← parseExpr ctx sc .uint (← field node "initialValue") - let rest ← wrapModifierParts ctx (sc.push id .uint) restPre post (inner.shift .uint 0) - pure (.local_ value rest) - else - let head ← parseStmts ctx sc [] none [node] - let rest ← wrapModifierParts ctx sc restPre post inner - pure (.seq head rest) - -private def wrapModifiers (ctx : ParseCtx) {Γ : Sol.Ctx} (sc : Scope Γ) - (invs : List Json) (body : Sol.Stmt ctx.L ctx.F Γ r) : - MetaM (Sol.Stmt ctx.L ctx.F Γ r) := do - match invs with - | [] => pure body - | inv :: rest => - needAt ctx.frontend.source inv ((← str (← field inv "kind")) == "modifierInvocation") - "unsupported modifier" - if let some args := field? inv "arguments" then - unless args.isNull do - needAt ctx.frontend.source inv ((← arr args).isEmpty) "modifier arguments" - let nameNode ← field inv "modifierName" - let rid ← int (← field nameNode "referencedDeclaration") - needAt ctx.frontend.source inv (rid >= 0) "unresolved modifier" - let some m := lookupMod ctx.frontend rid.toNat | failAt ctx.frontend.source inv "unresolved modifier" - let mbody := field? m.node "body" |>.getD Json.null - let nph := countPlaceholders mbody - if nph > 1 then failAt ctx.frontend.source m.node "modifier with two placeholders" - if nph == 0 then failAt ctx.frontend.source m.node "modifier missing placeholder" - let stmts := (field? mbody "statements").bind (·.getArr?.toOption) |>.getD #[] - let (pre, post) ← splitModifierBody stmts.toList - let inner ← wrapModifiers ctx sc rest body - for s in pre do - if (← nodeKind s) == "Return" then - failAt ctx.frontend.source s "modifier prelude return skips function body" - let mctx := { ctx with superFrom := m.contractId } - if ← pre.anyM fun s => (· == "VariableDeclarationStatement") <$> nodeKind s then - wrapModifierParts mctx sc pre post inner - else - let preS ← parseStmts mctx sc [] none pre - let postS ← parseStmts mctx sc [] none post - pure (.seq preS (.block inner postS)) - -private def parseBody (ctx : ParseCtx) (fn : FnInfo) : MetaM Expr := do - let Γ : Sol.Ctx := fn.paramIds.reverse.map fun p => p.2.1 - let sc0 := scopeOf fn.paramIds Γ - let statements := - match field? fn.node "body" with - | some body => (field? body "statements").bind (·.getArr?.toOption) |>.getD #[] - | none => #[] - let invs := (← fnModifierInvocations fn).toList - match fn.namedReturn with - | some (id, ty, _) => - let sc := sc0.push id ty - let defaultE : Sol.Expr ctx.L ctx.F Γ ty := - match ty with - | .uint => .lit 0 - | .addr => .zero - | .pair _ => .pair (.lit 0) .zero - let inner ← parseStmts ctx sc fn.rets (some id) statements.toList - let wrapped : Sol.Stmt ctx.L ctx.F Γ fn.rets := .local_ defaultE inner - let withMods ← wrapModifiers ctx sc0 invs wrapped - pure (toExpr withMods) - | none => - let body ← parseStmts ctx sc0 fn.rets none statements.toList - let withMods ← wrapModifiers ctx sc0 invs body - pure (toExpr withMods) - -private def importFrontend (ns : Name) (frontend : Frontend) : MetaM Unit := do - let L := layoutOf frontend.fields - let order ← topoSort frontend - let mut registered : List (FieldInfo × Expr) := [] - for f in frontend.fields do - let ty ← match f.sty with - | .mapping => mkArrow address uint - | .addr => pure address - | .scalar => pure uint - let slot ← mkAppOptM ``Verity.StorageSlot.mk #[some ty, some (mkNatLit f.slot)] - let name := ns ++ Name.mkSimple f.name - register name slot - registered := registered ++ [(f, mkConst name)] - let slotsE ← slotsTerm (registered.map fun h => (h.1.sty, h.2)) - let state := mkConst ``Verity.ContractState - let storageView := mkConst (ns ++ `Storage) - let view ← withLocalDeclD `s state fun s => do - let mut args : Array Expr := #[] - for h in registered do - let slot ← mkAppM ``Verity.StorageSlot.slot #[h.2] - match h.1.sty with - | .mapping => - let reader ← withLocalDeclD `k address fun k => - mkLambdaFVars #[k] (mkAppN (mkConst ``Verity.ContractState.readMap) #[s, slot, k]) - args := args.push reader - | .addr => - args := args.push (mkAppN (mkConst ``Verity.ContractState.readAddrSlot) #[s, slot]) - | .scalar => - args := args.push (mkAppN (mkConst ``Verity.ContractState.readSlot) #[s, slot]) - mkLambdaFVars #[s] (← mkAppM (ns ++ `Storage ++ `mk) args) - register (ns ++ `view) view (some (← mkArrow state storageView)) - let opaqueVal : List (String × Nat × String) := - frontend.opaqueEntries.map fun o => (o.label, o.slot, o.typeLabel) - register (ns ++ `opaqueFields) (toExpr opaqueVal) (simp := false) - register (ns ++ `registeredSources) (toExpr registeredSources) (simp := false) - let mut getterEntries : Array (Name × List (Name × Expr)) := #[] - for h in registered do - if let some getter := h.1.getter then - let gname := ns ++ Name.mkSimple getter - match h.1.structId, h.1.memberIndex with - | some _, some 0 => - let members := registered.filter (fun g => g.1.id == h.1.id && g.1.structId.isSome) - needAt frontend.source frontend.ast (members.length == 2) "storage type mismatch" - let some amountH := members.find? (fun g => g.1.memberIndex == some 0) - | throwError "unresolved struct getter" - let some whoH := members.find? (fun g => g.1.memberIndex == some 1) - | throwError "unresolved struct getter" - let amount ← mkAppM ``Verity.getStorage #[amountH.2] - let who ← mkAppM ``Verity.getStorageAddr #[whoH.2] - let value ← mkAppM ``Sol.nonpayable - #[← mkAppM ``Verity.bind #[amount, ← withLocalDeclD `x uint fun x => do - mkLambdaFVars #[x] (← mkAppM ``Verity.bind #[who, ← withLocalDeclD `y address fun y => do - mkLambdaFVars #[y] (← mkAppM ``Verity.pure #[← mkAppM ``Prod.mk #[x, y]])])]] - register gname value - getterEntries := getterEntries.push (gname, []) - | some _, some _ => pure () - | _, _ => - match h.1.sty with - | .mapping => - let value ← withLocalDeclD `account address fun account => do - let getterFn ← mkAppM ``Verity.getMapping #[h.2, account] - mkLambdaFVars #[account] (← mkAppM ``Sol.nonpayable #[getterFn]) - register gname value - getterEntries := getterEntries.push (gname, [(`account, address)]) - | .addr => - let value ← mkAppM ``Sol.nonpayable #[← mkAppM ``Verity.getStorageAddr #[h.2]] - register gname value - getterEntries := getterEntries.push (gname, []) - | .scalar => - let value ← mkAppM ``Sol.nonpayable #[← mkAppM ``Verity.getStorage #[h.2]] - register gname value - getterEntries := getterEntries.push (gname, []) - let mut fnHandles : List (Sol.Sig × Expr) := [] - let mut fnInfos : List FnInfo := [] - let mut functionEntries : Array (Name × List (Name × Expr)) := #[] - for fn in order do - let F : Sol.Fns := fnHandles.map (·.1) - let fnsE ← fnsTerm fnHandles - let ctx : ParseCtx := { frontend, L, F, registered := fnInfos, current := fn, superFrom := fn.contractId } - let Γ : Sol.Ctx := fn.paramIds.reverse.map fun p => p.2.1 - let bodyE ← parseBody ctx fn - let meaningHead := - if fn.isEntry then mkConst ``Sol.Fn.meaning else mkConst ``Sol.Fn.bodyMeaning - let value ← withParams fn.paramIds [] fun xs => do - let envE ← envTerm xs - pure (mkAppN meaningHead - #[toExpr L, toExpr F, toExpr Γ, toExpr fn.rets, bodyE, slotsE, fnsE, envE]) - let mut type := ← mkAppM ``Verity.Contract #[resultType fn.rets] - for p in fn.paramIds.reverse do - type ← mkArrow (valueType p.2.1) type - let fname := ns ++ Name.mkSimple fn.leanIdent - register fname value (some type) - let binders := fn.paramIds.map fun p => (Name.mkSimple p.2.2, valueType p.2.1) - if fn.isEntry then - if let some top := mostDerived frontend fn.name fn.paramTys then - if top.id == fn.id then - functionEntries := functionEntries.push (fname, binders) - fnHandles := fnHandles ++ [(fn.sig, mkConst fname)] - fnInfos := fnInfos ++ [fn] - -- Entry-point order: target public/external in source order, then each base - -- in linearization order, then getters in layout order. - let mut orderedEntries : Array (Name × List (Name × Expr)) := #[] - for cid in frontend.linearization do - for fn in frontend.functions do - if fn.contractId == cid && fn.isEntry then - if let some top := mostDerived frontend fn.name fn.paramTys then - if top.id == fn.id then - let fname := ns ++ Name.mkSimple fn.leanIdent - if let some entry := functionEntries.find? (·.1 == fname) then - unless orderedEntries.any (fun e => e.1 == fname) do - orderedEntries := orderedEntries.push entry - let entries := orderedEntries ++ getterEntries - let step ← withLocalDeclD `s state fun s => - withLocalDeclD `s' state fun s' => do - let mut disjuncts : Array Expr := #[] - for (ename, binders) in entries do - disjuncts := disjuncts.push (← mkEntryDisjunct s s' ename binders) - if disjuncts.isEmpty then throwError "imported contract has no entry points" - let mut body := disjuncts.back! - for i in (List.range (disjuncts.size - 1)).reverse do - body := mkApp2 (mkConst ``Or) disjuncts[i]! body - mkLambdaFVars #[s, s'] body - let stepType ← mkArrow state (← mkArrow state (mkSort 0)) - register (ns ++ `step) step (some stepType) (simp := false) - register (ns ++ `sourceDigest) (mkStrLit frontend.digest) (simp := false) - -private def compileFrontend (root source : System.FilePath) (targetName? : Option String) : MetaM Frontend := do - let canonicalRoot ← IO.FS.realPath root - let canonicalSource ← IO.FS.realPath source - unless canonicalSource.toString.startsWith (canonicalRoot.toString ++ "/") do - throwError "source outside package" - let mut matched : Option (String × String) := none - for pair in registeredSources do - let expected ← IO.FS.realPath (canonicalRoot / pair.2) - if canonicalSource == expected then matched := some pair - let some (logicalPath, _) := matched | throwError "unregistered source or source outside package" - let compiler := canonicalRoot / ".lake/solidity-import/solc" - verifyCompiler compiler - let versionOut ← IO.Process.output { cmd := compiler.toString, args := #["--version"] } - unless versionOut.exitCode == 0 && - acceptedSolcVersionOutputs.contains versionOut.stdout.trimAscii.toString do - throwError "compiler version mismatch" - verifyCompiler compiler - let sourceBytes ← IO.FS.readBinFile canonicalSource - let sourceText ← match String.fromUTF8? sourceBytes with - | some text => pure text - | none => throwError "Solidity source is not UTF-8" - let settings := Json.mkObj [ - ("optimizer", Json.mkObj [("enabled", false)]), - ("viaIR", false), ("evmVersion", "cancun"), ("remappings", Json.arr #[]), - ("outputSelection", Json.mkObj [("*", Json.mkObj [ - ("", Json.arr #["ast"]), ("*", Json.arr #["storageLayout"])])])] - let input := Json.mkObj [("language", "Solidity"), - ("sources", Json.mkObj [(logicalPath, Json.mkObj [("content", sourceText)])]), - ("settings", settings)] - let output ← IO.Process.output - { cmd := compiler.toString, args := #["--standard-json", "--no-import-callback"] } - (some input.compress) - unless output.exitCode == 0 do throwError "solc failed: {output.stderr}" - verifyCompiler compiler - let importerDir := canonicalRoot / "Contracts/VaultFromSolidity/Importer" - let importerText ← IO.FS.readFile (importerDir / "Importer.lean") - let syntaxText ← IO.FS.readFile (importerDir / "Syntax.lean") - let semanticsText ← IO.FS.readFile (importerDir / "Semantics.lean") - parseCompilerOutput canonicalSource logicalPath sourceBytes output.stdout - (importerText ++ syntaxText ++ semanticsText) targetName? - -syntax (name := solidityContract) "solidity_contract " ident " from " str : command -syntax (name := solidityContractNamed) "solidity_contract " ident " from " str " contract " str : command - -private def elabSolidityContractCore (alias : Name) (rel : String) (targetName? : Option String) - (_stx : Syntax) : CommandElabM Unit := do - let saved ← getEnv - try - if debug.skipKernelTC.get (← getOptions) then throwError "kernel checking must be enabled" - let authored ← IO.FS.realPath (← getFileName) - let source := authored.parent.getD "." / rel - let mut root := authored.parent.getD "." - while !(← (root / "lakefile.lean").pathExists) do - let some parent := root.parent | throwError "package root not found" - if parent == root then throwError "package root not found" - root := parent - let frontend ← liftTermElabM <| compileFrontend root source targetName? - let ns := (← getCurrNamespace) ++ alias - let order ← liftTermElabM <| topoSort frontend - liftTermElabM <| checkCollisions ns frontend order - let storageStx ← storageStructure alias frontend - withScope (fun scope => { scope with opts := Elab.async.set scope.opts false }) do - elabCommand storageStx - if (← get).messages.hasErrors then - throwError "storage view elaboration failed" - liftTermElabM <| withOptions (Elab.async.set · false) (importFrontend ns frontend) - catch e => - setEnv saved - throw e - -@[command_elab solidityContract] def elabSolidityContract : CommandElab := fun stx => do - elabSolidityContractCore stx[1].getId stx[3].isStrLit?.get! none stx - -@[command_elab solidityContractNamed] def elabSolidityContractNamed : CommandElab := fun stx => do - elabSolidityContractCore stx[1].getId stx[3].isStrLit?.get! stx[5].isStrLit? stx - -end SolidityImporter diff --git a/Contracts/VaultFromSolidity/Importer/Semantics.lean b/Contracts/VaultFromSolidity/Importer/Semantics.lean deleted file mode 100644 index 97229b4878..0000000000 --- a/Contracts/VaultFromSolidity/Importer/Semantics.lean +++ /dev/null @@ -1,244 +0,0 @@ -/- - What each accepted Solidity construct means, in Verity terms. - - This is the single definition of the translation: `Importer.lean` parses solc's - typed AST into the closed inductive in `Syntax.lean` and registers each entry - point as `Fn.meaning body slots fns env`. Nothing here inspects JSON, and nothing - here is generated source -- the registered value is a kernel-checked Lean term. - - Statement order matches the Solidity source order exactly, because the proofs - pin the write order the contract produces. Internal calls evaluate arguments - left to right, then run the callee body under the caller's `msg.sender` / - `msg.value`. `Expr.call` is `view`/`pure` only; effectful internals are - `Stmt.callStmt`. Pinned solc 0.8.x legacy codegen (`viaIR=false`) evaluates - an effectful call before the other operand / `+=` old-read, so modelling - those calls as left-to-right `Expr.call` would disagree with bytecode. - - Function modifiers are inlined at parse time; this file never sees `_`. - `Stmt.block inner post` runs `inner` and then `post`, so a function `return` - exits the block only and postludes still run. A revert anywhere aborts the - whole call. --/ - -import Contracts.VaultFromSolidity.Importer.Syntax -import Verity.Stdlib.Math -import Verity.Core.SolidityImportAttr - -namespace SolidityImporter.Sol - -open Lean - -/-- Two-member user struct as a product. Shared with the importer's Lean binder -type so `Env.cons` sees the same type as `Ty.pair.denote`. -/ -abbrev Pair := Verity.Core.Uint256 × Verity.Core.Address - -/-- `uint256` denotes Verity's `Uint256`, `address` its `Address`, two-member -user structs their product (the solc id is erased at denote). -/ -@[reducible] def Ty.denote : Ty → Type - | .uint => Verity.Core.Uint256 - | .addr => Verity.Core.Address - | .pair _ => Pair - -/-- A function body returns nothing, one value, or (later) a tuple. -/ -@[reducible] def Ret.denote : Ret → Type - | [] => Unit - | t :: [] => t.denote - | t :: ts => t.denote × Ret.denote ts - -/-- A state variable is a named `StorageSlot`, scalar, address, or `address`-keyed. -/ -@[reducible] def StorageTy.denote : StorageTy → Type - | .scalar => Verity.StorageSlot Verity.Core.Uint256 - | .mapping => Verity.StorageSlot (Verity.Core.Address → Verity.Core.Uint256) - | .addr => Verity.StorageSlot Verity.Core.Address - -/-- Parameter/local values, in the same de Bruijn order as `Ctx`. -/ -inductive Env : Ctx → Type where - | nil : Env [] - | cons : (x : t.denote) → (rest : Env Γ) → Env (t :: Γ) - -/-- The imported state-variable handles, in the same order as `Layout`. -/ -inductive Slots : Layout → Type where - | nil : Slots [] - | cons : (s : st.denote) → (rest : Slots L) → Slots (st :: L) - -/-- Declaration-order values of a call's arguments, before they are packed -into a de Bruijn `Env`. -/ -inductive Vals : List Ty → Type where - | nil : Vals [] - | cons : t.denote → Vals ts → Vals (t :: ts) - -/-- Already-registered function meanings: each takes a de Bruijn environment -of its parameters and returns a `Contract` of its result type. Slots are -baked into the registered Lean constants. -/ -inductive FnEnv : Fns → Type where - | nil : FnEnv [] - | cons : (Env σ.params → Verity.Contract (Ret.denote σ.rets)) → - FnEnv F → FnEnv (σ :: F) - -/-- The value bound at a variable. -/ -def Env.get : (env : Env Γ) → (v : Var Γ t) → t.denote - | .cons x _, .here => x - | .cons _ rest, .there v => Env.get rest v - -/-- The storage slot a state-variable handle refers to. -/ -def Slots.get : (slots : Slots L) → (s : SVar L st) → st.denote - | .cons s _, .here => s - | .cons _ rest, .there s => Slots.get rest s - -/-- The meaning of an already-registered function. -/ -def FnEnv.get : (fns : FnEnv F) → (f : FVar F σ) → - Env σ.params → Verity.Contract (Ret.denote σ.rets) - | .cons m _, .here => m - | .cons _ rest, .there f => FnEnv.get rest f - -/-- Pack declaration-order values into a de Bruijn environment by the same -`reverseAux` walk `List.reverse` uses, so the type is definitionally -`Env ts.reverse`. Last declared parameter is innermost. -/ -def reverseAuxEnv : {acc : Ctx} → (ts : List Ty) → Vals ts → Env acc → - Env (List.reverseAux ts acc) - | _, [], .nil, env => env - | acc, _ :: ts, .cons x vs, env => reverseAuxEnv (acc := _ :: acc) ts vs (.cons x env) - -/-- Pack declaration-order values into a de Bruijn environment. -/ -def envOfVals {ts : List Ty} (vs : Vals ts) : Env ts.reverse := - reverseAuxEnv ts vs .nil - -/-- Solidity 0.8 checked `+`/`-` on `uint256`: overflow/underflow reverts with -`Panic(0x11)` instead of wrapping. -/ -def checked (op : ArithOp) (a b : Verity.Core.Uint256) : Verity.Contract Verity.Core.Uint256 := - match op with - | .add => Verity.Stdlib.Math.requireSomeUint (Verity.Stdlib.Math.safeAdd a b) "Panic(0x11)" - | .sub => Verity.Stdlib.Math.requireSomeUint (Verity.Stdlib.Math.safeSub a b) "Panic(0x11)" - -mutual -/-- The meaning of an expression: a `Contract` producing its value. -/ -def Expr.meaning {L : Layout} {F : Fns} {Γ : Ctx} {t : Ty} - (e : Expr L F Γ t) (slots : Slots L) (fns : FnEnv F) (env : Env Γ) : - Verity.Contract t.denote := - match e with - | .var v => Verity.pure (Env.get env v) - | .sender => Verity.msgSender - | .zero => Verity.pure Verity.zeroAddress - | .lit n => Verity.pure (Verity.Core.Uint256.ofNat n) - | .load s => Verity.getStorage (Slots.get slots s) - | .loadAddr s => Verity.getStorageAddr (Slots.get slots s) - | .index m key => - Verity.bind (Expr.meaning key slots fns env) (fun a => - Verity.getMapping (Slots.get slots m) a) - | .arith op a b => - Verity.bind (Expr.meaning a slots fns env) (fun x => - Verity.bind (Expr.meaning b slots fns env) (fun y => checked op x y)) - | .pair a b => - Verity.bind (Expr.meaning a slots fns env) (fun x => - Verity.bind (Expr.meaning b slots fns env) (fun y => Verity.pure (x, y))) - | .pairRev a b => - Verity.bind (Expr.meaning a slots fns env) (fun y => - Verity.bind (Expr.meaning b slots fns env) (fun x => Verity.pure (x, y))) - | .fst e => - Verity.bind (Expr.meaning e slots fns env) (fun vs => Verity.pure vs.1) - | .snd e => - Verity.bind (Expr.meaning e slots fns env) (fun vs => Verity.pure vs.2) - | .call fvar args => - Verity.bind (Args.eval args slots fns env) (fun vals => - FnEnv.get fns fvar (envOfVals vals)) - -/-- Evaluate call arguments left to right in declaration order. -/ -def Args.eval {L : Layout} {F : Fns} {Γ : Ctx} {ts : List Ty} - (args : Args L F Γ ts) (slots : Slots L) (fns : FnEnv F) (env : Env Γ) : - Verity.Contract (Vals ts) := - match args with - | .nil => Verity.pure Vals.nil - | .cons e rest => - Verity.bind (Expr.meaning e slots fns env) (fun x => - Verity.bind (Args.eval rest slots fns env) (fun vs => - Verity.pure (Vals.cons x vs))) - -/-- `lhs op= rhs` followed by `k`, in source evaluation order: for `+=`/`-=` the -mapping key (if any) is read first, then the old value, then the right-hand side, -then the checked arithmetic, then the write, then the rest of the body. -/ -def assignWith {α : Type} (read : Verity.Contract Verity.Core.Uint256) - (write : Verity.Core.Uint256 → Verity.Contract Unit) : - AssignOp → Verity.Contract Verity.Core.Uint256 → Verity.Contract α → Verity.Contract α - | .set, rhs, k => Verity.bind rhs (fun v => Verity.bind (write v) (fun _ => k)) - | .add, rhs, k => - Verity.bind read (fun old => - Verity.bind rhs (fun v => - Verity.bind (checked .add old v) (fun w => Verity.bind (write w) (fun _ => k)))) - | .sub, rhs, k => - Verity.bind read (fun old => - Verity.bind rhs (fun v => - Verity.bind (checked .sub old v) (fun w => Verity.bind (write w) (fun _ => k)))) - -/-- The meaning of a statement: the rest of the body is its continuation. -/ -def Stmt.meaning {L : Layout} {F : Fns} {Γ : Ctx} {r : Ret} - (s : Stmt L F Γ r) (slots : Slots L) (fns : FnEnv F) (env : Env Γ) : - Verity.Contract (Ret.denote r) := - match s with - | .done => Verity.pure () - | .ret e => Expr.meaning e slots fns env - | .assign lv op rhs rest => - match lv with - | .scalar sv => - assignWith (Verity.getStorage (Slots.get slots sv)) - (fun v => Verity.setStorage (Slots.get slots sv) v) op - (Expr.meaning rhs slots fns env) (Stmt.meaning rest slots fns env) - | .mapping sv key => - Verity.bind (Expr.meaning key slots fns env) (fun a => - assignWith (Verity.getMapping (Slots.get slots sv) a) - (fun v => Verity.setMapping (Slots.get slots sv) a v) op - (Expr.meaning rhs slots fns env) (Stmt.meaning rest slots fns env)) - | .assignAddr sv rhs rest => - Verity.bind (Expr.meaning rhs slots fns env) (fun a => - Verity.bind (Verity.setStorageAddr (Slots.get slots sv) a) (fun _ => - Stmt.meaning rest slots fns env)) - | .local_ e rest => - Verity.bind (Expr.meaning e slots fns env) - (fun x => Stmt.meaning rest slots fns (Env.cons x env)) - | .guard a b msg rest => - Verity.bind (Expr.meaning a slots fns env) (fun x => - Verity.bind (Expr.meaning b slots fns env) (fun y => - Verity.bind (Verity.require (Nat.ble y.val x.val) msg) - (fun _ => Stmt.meaning rest slots fns env))) - | .guardAddrNe a b msg rest => - Verity.bind (Expr.meaning a slots fns env) (fun x => - Verity.bind (Expr.meaning b slots fns env) (fun y => - Verity.bind (Verity.require (decide (x = y)) msg) - (fun _ => Stmt.meaning rest slots fns env))) - | .callStmt fvar args rest => - Verity.bind (Args.eval args slots fns env) (fun vals => - Verity.bind (FnEnv.get fns fvar (envOfVals vals)) (fun _ => - Stmt.meaning rest slots fns env)) - | .seq pre rest => - Verity.bind (Stmt.meaning pre slots fns env) (fun _ => - Stmt.meaning rest slots fns env) - | .block inner post => - Verity.bind (Stmt.meaning inner slots fns env) (fun v => - Verity.bind (Stmt.meaning post slots fns env) (fun _ => Verity.pure v)) -end - -/-- Every imported entry point is non-payable. Internal calls do not re-apply -this guard; they run the callee body under the caller's `msg.value`. -/ -def nonpayable {α : Type} (m : Verity.Contract α) : Verity.Contract α := - Verity.bind Verity.msgValue (fun value => - Verity.bind (Verity.require (Nat.beq value.val 0) "Nonpayable") (fun _ => m)) - -/-- The meaning of a whole function: its body, under the non-payable guard. -/ -def Fn.meaning {L : Layout} {F : Fns} {Γ : Ctx} {r : Ret} - (body : Stmt L F Γ r) (slots : Slots L) (fns : FnEnv F) (env : Env Γ) : - Verity.Contract (Ret.denote r) := - nonpayable (Stmt.meaning body slots fns env) - -/-- Body meaning without the non-payable guard, used for internal functions. -/ -def Fn.bodyMeaning {L : Layout} {F : Fns} {Γ : Ctx} {r : Ret} - (body : Stmt L F Γ r) (slots : Slots L) (fns : FnEnv F) (env : Env Γ) : - Verity.Contract (Ret.denote r) := - Stmt.meaning body slots fns env - --- Every imported entry point is registered as `Fn.meaning` applied to the parsed --- term, so the proofs need these definitions to unfold with the rest of the --- `solidity_import` simp set. -attribute [solidity_import] Fn.meaning Fn.bodyMeaning nonpayable Stmt.meaning assignWith - Expr.meaning Args.eval checked Env.get Slots.get FnEnv.get envOfVals - Ty.denote Ret.denote StorageTy.denote - -end SolidityImporter.Sol diff --git a/Contracts/VaultFromSolidity/Importer/Syntax.lean b/Contracts/VaultFromSolidity/Importer/Syntax.lean deleted file mode 100644 index d0b66e116a..0000000000 --- a/Contracts/VaultFromSolidity/Importer/Syntax.lean +++ /dev/null @@ -1,253 +0,0 @@ -/- - The Solidity subset the Vault importer accepts, as an intrinsically typed - inductive family. - - Ill-typed programs are unrepresentable: reading a `uint256` state variable - yields a `uint`, a mapping index must be an `addr`, a `uint256` local can only - be bound to a `uint`, and `Stmt L F Γ r` only ever produces a value of type `r`. - The parser in `Importer.lean` is the only producer of these terms and - `Semantics.lean` is the only place that gives them meaning. - - `Ctx` is a de Bruijn context: index 0 is the innermost binder. `Layout` is the - imported state-variable order, which the importer keeps in lockstep with the - `Slot` handles it registers. `Fns` is the callee-first function table: - a body may only mention functions already registered, so recursion is - unrepresentable. --/ - -import Lean - -open Lean - -namespace SolidityImporter.Sol - -/-- Types of the accepted subset: `uint256`, `address`, and a two-member -user struct as `uint256 × address`, indexed by the solc struct id so -distinct shapes-alike structs stay distinct in signatures. -/ -inductive Ty where - | uint : Ty - | addr : Ty - | pair : Nat → Ty - deriving DecidableEq, Repr, Inhabited, ToExpr - -/-- What an imported function returns. Empty is `void`; a singleton is the -POC's one named or unnamed return. Multi-value returns wait for a later slice. -/ -abbrev Ret := List Ty - -/-- Shape of an imported state variable. -/ -inductive StorageTy where - | scalar : StorageTy - | mapping : StorageTy - | addr : StorageTy - deriving DecidableEq, Repr, Inhabited, ToExpr - -/-- Parameter/local context, de Bruijn order. -/ -abbrev Ctx := List Ty - -/-- Imported state variables, in importer field order. -/ -abbrev Layout := List StorageTy - -/-- A function signature: parameters in de Bruijn order (last declared -innermost) and the return list. -/ -structure Sig where - params : Ctx - rets : Ret - deriving DecidableEq, Repr, Inhabited, ToExpr - -/-- Already-registered function signatures, callee-first. -/ -abbrev Fns := List Sig - -/-- A parameter or local at a given type in a context. -/ -inductive Var : Ctx → Ty → Type where - | here : Var (t :: Γ) t - | there : Var Γ t → Var (t' :: Γ) t - deriving ToExpr - -/-- A state variable handle of a given storage shape. -/ -inductive SVar : Layout → StorageTy → Type where - | here : SVar (st :: L) st - | there : SVar L st → SVar (st' :: L) st - deriving ToExpr - -/-- A handle of an already-registered function. -/ -inductive FVar : Fns → Sig → Type where - | here : FVar (σ :: F) σ - | there : FVar F σ → FVar (σ' :: F) σ - deriving ToExpr - -/-- Checked arithmetic operators: Solidity's `+` and `-` on `uint256`. -/ -inductive ArithOp where - | add : ArithOp - | sub : ArithOp - deriving DecidableEq, Repr, Inhabited, ToExpr - -/-- Assignment operators: `=`, `+=` and `-=`. -/ -inductive AssignOp where - | set : AssignOp - | add : AssignOp - | sub : AssignOp - deriving DecidableEq, Repr, Inhabited, ToExpr - -/-- The `n`-th variable of a context, if the context is that deep. -/ -def Var.ofIndex : (Γ : Ctx) → (n : Nat) → Option (Σ t, Var Γ t) - | [], _ => none - | _ :: _, 0 => some ⟨_, .here⟩ - | _ :: Γ, n + 1 => (Var.ofIndex Γ n).map fun v => ⟨v.1, .there v.2⟩ - -/-- The `n`-th state variable of a layout, if the layout is that deep. -/ -def SVar.ofIndex : (L : Layout) → (n : Nat) → Option (Σ st, SVar L st) - | [], _ => none - | _ :: _, 0 => some ⟨_, .here⟩ - | _ :: L, n + 1 => (SVar.ofIndex L n).map fun s => ⟨s.1, .there s.2⟩ - -/-- The `n`-th already-registered function, if the table is that long. -/ -def FVar.ofIndex : (F : Fns) → (n : Nat) → Option (Σ σ, FVar F σ) - | [], _ => none - | _ :: _, 0 => some ⟨_, .here⟩ - | _ :: F, n + 1 => (FVar.ofIndex F n).map fun f => ⟨f.1, .there f.2⟩ - -/-- Declaration-order argument list (leftmost first). `Sig.params` is the -de Bruijn reverse of this list. -/ -abbrev DeclParams := List Ty - -mutual -/-- `uint256`/`address` expressions: locals, `msg.sender`, literals, state -reads, checked `+`/`-`, and internal calls that return one value. -/ -inductive Expr (L : Layout) (F : Fns) (Γ : Ctx) : Ty → Type where - /-- A parameter or local. -/ - | var : Var Γ t → Expr L F Γ t - /-- `msg.sender`. -/ - | sender : Expr L F Γ .addr - /-- The zero address, used for named-return defaults. -/ - | zero : Expr L F Γ .addr - /-- A numeric literal; the parser enforces `n < 2^256`. -/ - | lit : Nat → Expr L F Γ .uint - /-- `uint256` state variable read. -/ - | load : SVar L .scalar → Expr L F Γ .uint - /-- `address` state variable read. -/ - | loadAddr : SVar L .addr → Expr L F Γ .addr - /-- `mapping(address => uint256)` read. -/ - | index : SVar L .mapping → Expr L F Γ .addr → Expr L F Γ .uint - /-- Checked `+`/`-` on `uint256`, reverting with `Panic(0x11)`. -/ - | arith : ArithOp → Expr L F Γ .uint → Expr L F Γ .uint → Expr L F Γ .uint - /-- Memory struct `{amount, who}` of solc struct `id`. Evaluates amount then who - (positional `Acc(a, b)` and named `Acc({amount, who})`). -/ - | pair : {id : Nat} → Expr L F Γ .uint → Expr L F Γ .addr → Expr L F Γ (.pair id) - /-- Same packing as `pair`, but evaluates who then amount. Named - `Acc({who, amount})` uses this so source-order eval matches pinned solc 0.8.33 - before the result is packed as `uint256 × address`. -/ - | pairRev : {id : Nat} → Expr L F Γ .addr → Expr L F Γ .uint → Expr L F Γ (.pair id) - /-- First struct member (`amount`). -/ - | fst : {id : Nat} → Expr L F Γ (.pair id) → Expr L F Γ .uint - /-- Second struct member (`who`). -/ - | snd : {id : Nat} → Expr L F Γ (.pair id) → Expr L F Γ .addr - /-- Internal call of an already-registered `view`/`pure` function that - returns one value. Arguments are in declaration order and evaluate left - to right. Effectful internals are `Stmt.callStmt` only: pinned solc 0.8.x - legacy codegen evaluates those calls before the other operand / old-read. -/ - | call : {ts : List Ty} → - FVar F ⟨ts.reverse, [t]⟩ → Args L F Γ ts → Expr L F Γ t - deriving ToExpr - -/-- Declaration-order arguments of an internal call. -/ -inductive Args (L : Layout) (F : Fns) (Γ : Ctx) : List Ty → Type where - | nil : Args L F Γ [] - | cons : Expr L F Γ t → Args L F Γ ts → Args L F Γ (t :: ts) - deriving ToExpr -end - -/-- Insert `t'` at de Bruijn depth `n` (0 = innermost). -/ -def insertAt : Nat → Ty → Ctx → Ctx - | 0, t', Γ => t' :: Γ - | n + 1, t', t :: Γ => t :: insertAt n t' Γ - | _ + 1, t', [] => [t'] - -/-- Shift a variable across a binder inserted at depth `n`. -/ -def Var.shift (t' : Ty) : (n : Nat) → {Γ : Ctx} → Var Γ t → Var (insertAt n t' Γ) t - | 0, _, v => .there v - | _ + 1, _, .here => .here - | n + 1, _, .there v => .there (Var.shift t' n v) - -mutual -/-- Shift an expression across a binder inserted at depth `n`. -/ -def Expr.shift (t' : Ty) {L : Layout} {F : Fns} (n : Nat) {Γ : Ctx} {t : Ty} : - Expr L F Γ t → Expr L F (insertAt n t' Γ) t - | .var v => .var (v.shift t' n) - | .sender => .sender - | .zero => .zero - | .lit k => .lit k - | .load s => .load s - | .loadAddr s => .loadAddr s - | .index s key => .index s (key.shift t' n) - | .arith op a b => .arith op (a.shift t' n) (b.shift t' n) - | .pair a b => .pair (a.shift t' n) (b.shift t' n) - | .pairRev a b => .pairRev (a.shift t' n) (b.shift t' n) - | .fst e => .fst (e.shift t' n) - | .snd e => .snd (e.shift t' n) - | .call fv args => .call fv (args.shift t' n) - -/-- Shift call arguments across a binder inserted at depth `n`. -/ -def Args.shift (t' : Ty) {L : Layout} {F : Fns} (n : Nat) {Γ : Ctx} {ts : List Ty} : - Args L F Γ ts → Args L F (insertAt n t' Γ) ts - | .nil => .nil - | .cons e rest => .cons (e.shift t' n) (rest.shift t' n) -end - -/-- Assignment targets: a `uint256` state variable or one mapping entry. -Address writes use `Stmt.assignAddr`. -/ -inductive LVal (L : Layout) (F : Fns) (Γ : Ctx) : Type where - | scalar : SVar L .scalar → LVal L F Γ - | mapping : SVar L .mapping → Expr L F Γ .addr → LVal L F Γ - deriving ToExpr - -/-- Statements of the accepted subset, in source order, ending in either -`done` (no return value) or `ret` of a single value. -/ -inductive Stmt (L : Layout) (F : Fns) : Ctx → Ret → Type where - /-- The end of a body that returns nothing. -/ - | done : Stmt L F Γ [] - /-- A terminal `return e;`. -/ - | ret : Expr L F Γ t → Stmt L F Γ [t] - /-- `lval op= rhs;` followed by the rest of the body. `+=`/`-=` are uint-only. -/ - | assign : LVal L F Γ → AssignOp → Expr L F Γ .uint → Stmt L F Γ r → Stmt L F Γ r - /-- `addr = rhs;` followed by the rest of the body. -/ - | assignAddr : SVar L .addr → Expr L F Γ .addr → Stmt L F Γ r → Stmt L F Γ r - /-- `uint256 x = e;` (or a parameter), followed by the rest of the body. -/ - | local_ : Expr L F Γ t → Stmt L F (t :: Γ) r → Stmt L F Γ r - /-- `if (a < b) revert E();` with `E()` stored verbatim, followed by the rest - of the body. -/ - | guard : Expr L F Γ .uint → Expr L F Γ .uint → String → Stmt L F Γ r → Stmt L F Γ r - /-- `if (a != b) revert E();` on addresses. -/ - | guardAddrNe : Expr L F Γ .addr → Expr L F Γ .addr → String → Stmt L F Γ r → Stmt L F Γ r - /-- Internal call of an already-registered void function, then the rest. -/ - | callStmt : {ts : List Ty} → - FVar F ⟨ts.reverse, []⟩ → Args L F Γ ts → Stmt L F Γ r → Stmt L F Γ r - /-- Run a void prefix, then the rest. Used for modifier preludes. -/ - | seq : Stmt L F Γ [] → Stmt L F Γ r → Stmt L F Γ r - /-- Run `inner`; its `ret`/`done` exits this block only. Then run void `post` - and yield `inner`'s value. Modifier postludes use this so they run after - an early return. -/ - | block : Stmt L F Γ r → Stmt L F Γ [] → Stmt L F Γ r - deriving ToExpr - -/-- Shift an assignment target across a binder inserted at depth `n`. -/ -def LVal.shift (t' : Ty) {L : Layout} {F : Fns} (n : Nat) {Γ : Ctx} : - LVal L F Γ → LVal L F (insertAt n t' Γ) - | .scalar s => .scalar s - | .mapping s key => .mapping s (key.shift t' n) - -/-- Shift a statement across a binder inserted at depth `n`. -/ -def Stmt.shift (t' : Ty) {L : Layout} {F : Fns} (n : Nat) {Γ : Ctx} {r : Ret} : - Stmt L F Γ r → Stmt L F (insertAt n t' Γ) r - | .done => .done - | .ret e => .ret (e.shift t' n) - | .assign lv op rhs rest => .assign (lv.shift t' n) op (rhs.shift t' n) (rest.shift t' n) - | .assignAddr s rhs rest => .assignAddr s (rhs.shift t' n) (rest.shift t' n) - | .local_ e rest => .local_ (e.shift t' n) (rest.shift t' (n + 1)) - | .guard a b msg rest => .guard (a.shift t' n) (b.shift t' n) msg (rest.shift t' n) - | .guardAddrNe a b msg rest => - .guardAddrNe (a.shift t' n) (b.shift t' n) msg (rest.shift t' n) - | .callStmt fv args rest => .callStmt fv (args.shift t' n) (rest.shift t' n) - | .seq pre rest => .seq (pre.shift t' n) (rest.shift t' n) - | .block inner post => .block (inner.shift t' n) (post.shift t' n) - -end SolidityImporter.Sol diff --git a/Contracts/VaultFromSolidity/Importer/scripts/solidity_importer_test.py b/Contracts/VaultFromSolidity/Importer/scripts/solidity_importer_test.py deleted file mode 100644 index d0b7395fad..0000000000 --- a/Contracts/VaultFromSolidity/Importer/scripts/solidity_importer_test.py +++ /dev/null @@ -1,591 +0,0 @@ -#!/usr/bin/env python3 -"""Acceptance checks for the Lean-only Solidity frontend. - -Python only orchestrates disposable builds and mutations. The production import -path is `Contracts/VaultFromSolidity/Importer/Importer.lean` -> pinned solc -> checked Lean declarations. -""" - -import hashlib -import os -from pathlib import Path -import re -import shutil -import subprocess -import sys -import tempfile - -ROOT = Path(__file__).resolve().parents[4] - - -def lake_binary() -> str: - found = shutil.which("lake") - if found: - return found - version = (ROOT / "lean-toolchain").read_text().strip().split(":")[-1] - candidates = [ - Path.home() / ".elan/toolchains" / f"leanprover--lean4---{version}" / "bin/lake", - Path("/home/claudine/.hermes/profiles/claudine/home/.elan/toolchains") - / f"leanprover--lean4---{version}" / "bin/lake", - ] - for candidate in candidates: - if candidate.is_file(): - return str(candidate) - raise RuntimeError("lake executable not found") - - -LAKE = lake_binary() -ENV = dict(os.environ) -SOLC_PIN = ( - "8324280591ce398d7e2722846bc10ecf1779b13a328ef97b687c92cd9c70801a" - if sys.platform == "darwin" - else "1274e5c4621ae478090c5a1f48466fd3c5f658ed9e14b15a0b213dc806215468" -) - - -def check(ok: bool, message: str) -> None: - if not ok: - raise AssertionError(message) - print("PASS " + message, flush=True) - - -def run(root: Path, args: list[str], success: bool = True, contains: str | None = None) -> str: - process = subprocess.run( - args, cwd=root, env=ENV, text=True, capture_output=True, timeout=300 - ) - output = process.stdout + process.stderr - if (process.returncode == 0) != success or (contains and contains not in output) or "PANIC" in output: - raise AssertionError(f"{args}: exit {process.returncode}\n{output}") - return output - - -def main() -> None: - importer_path = ROOT / "Contracts/VaultFromSolidity/Importer/Importer.lean" - importer_text = importer_path.read_text() - syntax_path = ROOT / "Contracts/VaultFromSolidity/Importer/Syntax.lean" - semantics_path = ROOT / "Contracts/VaultFromSolidity/Importer/Semantics.lean" - syntax_text = syntax_path.read_text() - semantics_text = semantics_path.read_text() - python_frontend = ROOT / "Contracts/VaultFromSolidity/Importer/scripts/solidity_importer.py" - check(not python_frontend.exists(), "no Python importer/frontend exists") - check("--standard-json" in importer_text and "--no-import-callback" in importer_text, - "Lean importer invokes pinned solc standard JSON with import callback disabled") - check("officialSolcSha256s" in importer_text and "compiler checksum mismatch" in importer_text, - "Lean importer enforces official compiler checksum allowlist and version pin") - check("0.8.33+commit.64118f21" in importer_text and "solcVersionPin" in importer_text, - "Lean importer pins the official solc release identity") - check(SOLC_PIN in importer_text and - "1274e5c4621ae478090c5a1f48466fd3c5f658ed9e14b15a0b213dc806215468" in importer_text, - "Lean importer allowlists official linux-amd64 and macosx-amd64 checksums") - check('cmd := "/usr/bin/sha256sum"' in importer_text and 'cmd := "sha256sum"' not in importer_text, - "Linux compiler checksum utility uses a fixed path, not PATH lookup") - check('cmd := "/usr/bin/shasum"' in importer_text and 'cmd := "shasum"' not in importer_text, - "macOS compiler checksum utility uses a fixed path, not PATH lookup") - check("parseExpr" in importer_text and "parseStmts" in importer_text and - "Expr.meaning" in semantics_text and "Stmt.meaning" in semantics_text, - "Solidity constructs have explicit Lean parser and semantics functions") - check("inductive Expr" in syntax_text and "inductive Stmt" in syntax_text and - "inductive LVal" in syntax_text, - "accepted Solidity subset is the closed inductive in Syntax.lean") - check(all(tag not in importer_text for tag in ('[\"read\"', '[\"write\"', '[\"guard\"')), - "no custom serialized JSON IR tags") - - with tempfile.TemporaryDirectory(prefix="verity-vault-check-", dir=ROOT.parent) as directory: - root = Path(directory) - for name in ("Verity", "Compiler", "Contracts", "scripts"): - shutil.copytree(ROOT / name, root / name) - for name in ("lakefile.lean", "lake-manifest.json", "lean-toolchain"): - shutil.copy2(ROOT / name, root / name) - shutil.copytree(ROOT / ".lake/build", root / ".lake/build") - (root / ".lake/solidity-import").mkdir(parents=True) - shutil.copy2(ROOT / ".lake/solidity-import/solc", root / ".lake/solidity-import/solc") - (root / ".lake/packages").symlink_to(ROOT / ".lake/packages", target_is_directory=True) - - source = root / "Contracts/VaultFromSolidity/Vault.sol" - original = source.read_bytes() - source_stamp = source.stat() - importer = root / "Contracts/VaultFromSolidity/Importer/Importer.lean" - importer_original = importer.read_bytes() - importer_stamp = importer.stat() - compiler = root / ".lake/solidity-import/solc" - compiler_original = compiler.read_bytes() - lean_sources = set(root.rglob("*.lean")) - - def edit_source(data: bytes) -> None: - source.write_bytes(data) - os.utime(source, ns=(source_stamp.st_atime_ns, source_stamp.st_mtime_ns)) - - def build(success: bool = True, contains: str | None = None) -> str: - return run(root, [LAKE, "build", "VaultFromSolidity"], success, contains) - - def artifacts() -> dict[str, tuple[int, str]]: - paths = list((root / ".lake/build/lib/lean/Contracts/VaultFromSolidity").rglob("*.olean")) - return { - str(path.relative_to(root)): ( - path.stat().st_mtime_ns, - hashlib.sha256(path.read_bytes()).hexdigest(), - ) - for path in paths - } - - def source_digest() -> str: - digest_probe = root / ".lake/solidity-import/SourceDigestProbe.lean" - try: - digest_probe.write_text( - "import Contracts.VaultFromSolidity.VaultFromSolidity\n" - "#eval Contracts.VaultFromSolidity.sourceDigest\n" - ) - output = run(root, [LAKE, "env", "lean", str(digest_probe)]) - match = re.search(r'"([0-9a-f]{64})"', output) - if match is None: - raise AssertionError("sourceDigest is not an auditable SHA-256 value") - check(True, "sourceDigest is an auditable SHA-256 value") - return match.group(1) - finally: - digest_probe.unlink(missing_ok=True) - - build() - check(True, "baseline lake build VaultFromSolidity") - - ux_probe = root / "Contracts/VaultFromSolidity/FrontendProbe.lean" - try: - ux_probe.write_text('''import Contracts.VaultFromSolidity.Importer.Importer -solidity_contract Vault from "Vault.sol" -#print Vault.deposit -''') - ux_output = run(root, [LAKE, "env", "lean", str(ux_probe)]) - check("def Vault.deposit" in ux_output and "Sol.Fn.meaning" in ux_output and - "Sol.Stmt.assign" in ux_output, - "documented solidity_contract Vault UX exposes #print Vault.deposit as its parsed AST") - finally: - ux_probe.unlink(missing_ok=True) - - proof = root / "Contracts/VaultFromSolidity/Proofs/ExecutionProof.lean" - proof_text = proof.read_text() - theorem_names = re.findall(r"^theorem\s+(\w+)", proof_text, re.M) - audit_file = root / ".lake/solidity-import/AxiomAudit.lean" - try: - audit_file.write_text( - "import Contracts.VaultFromSolidity.Proofs.ExecutionProof\n" - + "\n".join( - "#print axioms Contracts.VaultFromSolidity.Proofs.ExecutionProof." + name - for name in theorem_names - ) - + "\n" - ) - audit = run(root, [LAKE, "env", "lean", str(audit_file)]) - finally: - audit_file.unlink(missing_ok=True) - entries = re.findall( - r"'Contracts.VaultFromSolidity.Proofs.ExecutionProof.(\w+)' depends on axioms: \[([^\]]*)\]", audit - ) - check(set(theorem_names) == {name for name, _ in entries}, - "every theorem appears in actual #print axioms output") - axioms = {a.strip() for _, values in entries for a in values.split(",") if a.strip()} - check(axioms <= {"propext", "Quot.sound"}, - "no project axioms or sorryAx: " + ", ".join(sorted(axioms))) - - probe = root / ".lake/solidity-import/RegistrationProbe.lean" - try: - probe.write_text('''import Contracts.VaultFromSolidity.VaultFromSolidity -import Contracts.VaultFromSolidity.Importer.Semantics -open Lean Elab Command -#print Contracts.VaultFromSolidity.deposit -#print SolidityImporter.Sol.Fn.meaning -#print SolidityImporter.Sol.assignWith -#print SolidityImporter.Sol.checked -#check fun (v : Contracts.VaultFromSolidity.Storage) => v.totalAssets -#print Contracts.VaultFromSolidity.view -#print Contracts.VaultFromSolidity.step -/-- Transitive used-constant closure. A pattern-matching definition over an indexed -family is compiled through a `.brecOn` recursor plus a `._f` companion, so the -Verity primitives a construct unfolds to live in those companions rather than in -the top-level constant's own body. -/ -partial def semanticsClosure (env : Environment) (roots : Array Name) : Array Name := - let rec go (seen todo : Array Name) : Array Name := - if todo.isEmpty then seen - else - let cur := todo.back! - let todo := todo.pop - if seen.contains cur then go seen todo - else - let seen := seen.push cur - let deps := match env.find? cur with - | some (.defnInfo i) => i.value.getUsedConstants - | some (.thmInfo i) => i.value.getUsedConstants - | _ => #[] - go seen (todo ++ deps) - go #[] roots -run_cmd do - for suffix in ["totalAssetsSlot", "totalSupplySlot", "shareBalancesSlot", - "deposit", "withdraw", "balanceOf", "totalAssets", "totalSupply", - "shareBalances", "sourceDigest", "Storage.totalAssets", - "Storage.totalSupply", "Storage.shareBalances", "view", "step"] do - let name := `Contracts.VaultFromSolidity ++ suffix.toName - let some (.defnInfo info) := (← getEnv).find? name - | throwError "not a transparent definition: {name}" - unless info.safety == .safe && !info.value.hasMVar && !info.value.hasFVar do - throwError "unsafe or unclosed definition: {name}" - for dep in info.value.getUsedConstants do - if dep.toString.startsWith "Contracts." && - !dep.toString.startsWith "Contracts.VaultFromSolidity." then - throwError "imported declaration depends on handwritten contract: {dep}" - let some (.inductInfo _) := (← getEnv).find? `Contracts.VaultFromSolidity.Storage - | throwError "Storage is not an inductive structure" - let some (.ctorInfo _) := (← getEnv).find? `Contracts.VaultFromSolidity.Storage.mk - | throwError "Storage.mk is not a constructor" - let some (.defnInfo deposit) := (← getEnv).find? `Contracts.VaultFromSolidity.deposit - | throwError "missing imported deposit" - -- The imported body is the parsed Solidity AST under `Sol.Fn.meaning`, so the - -- constructors it uses are the source's constructs. - for dep in [``SolidityImporter.Sol.Fn.meaning, ``SolidityImporter.Sol.LVal.mapping, - ``SolidityImporter.Sol.LVal.scalar, ``SolidityImporter.Sol.AssignOp.add] do - unless deposit.value.getUsedConstants.contains dep do - throwError "imported deposit does not go through the parsed AST: {dep}" - -- ... and that meaning still bottoms out in the Verity primitives, reached - -- through the semantics definitions (including their generated companions). - let semantics := semanticsClosure (← getEnv) #[``SolidityImporter.Sol.Fn.meaning, - ``SolidityImporter.Sol.Stmt.meaning, ``SolidityImporter.Sol.assignWith, - ``SolidityImporter.Sol.checked, ``SolidityImporter.Sol.Expr.meaning] - for dep in [``Verity.setMapping, ``Verity.setStorage, ``Verity.Stdlib.Math.safeAdd] do - unless semantics.contains dep do - throwError "semantics does not bottom out in Verity primitive: {dep}" - logInfo "CHECKED_TRANSPARENT_DECLARATIONS" -solidity_contract Existing from "../../Contracts/VaultFromSolidity/Vault.sol" -run_cmd do - let original ← getEnv - let mut rejected := false - try - SolidityImporter.elabSolidityContract - (← `(command| solidity_contract $(mkIdent `Existing):ident from "../../Contracts/VaultFromSolidity/Vault.sol")) - catch _ => rejected := true - unless rejected do throwError "duplicate alias accepted" - let some (.defnInfo before) := original.find? `Existing.deposit - | throwError "missing initial declaration" - let some (.defnInfo after) := (← getEnv).find? `Existing.deposit - | throwError "lost initial declaration" - unless before.value == after.value && before.type == after.type do - throwError "duplicate alias changed prior declaration" - logInfo "DUPLICATE_ALIAS_REJECTED" -''') - output = run(root, [LAKE, "env", "lean", str(probe)]) - check("CHECKED_TRANSPARENT_DECLARATIONS" in output and - "DUPLICATE_ALIAS_REJECTED" in output, - "safe transparent readable definitions and collision rollback") - check("SolidityImporter.Sol.Fn.meaning" in output and - "SolidityImporter.Sol.Stmt.assign" in output and - "SolidityImporter.Sol.LVal.mapping" in output, - "#print deposit exposes the parsed Solidity AST under its meaning") - check("Verity.bind" in output and "SolidityImporter.Sol.checked" in output and - "safeAdd" in output, - "#print of the semantics exposes the Verity primitives it bottoms out in") - check("fun v => v.totalAssets : Contracts.VaultFromSolidity.Storage → Verity.Uint256" in output, - "named storage view supports v.totalAssets dot notation") - check("def Contracts.VaultFromSolidity.view" in output and - "shareBalancesSlot.slot" in output and "readMap" in output and - "readSlot" in output, - "#print view constructs Storage from named slot handles") - check("def Contracts.VaultFromSolidity.step" in output and - "deposit" in output and "totalSupply" in output and - "∃" in output, - "#print step is the entry-point relation") - finally: - probe.unlink(missing_ok=True) - - before = artifacts() - build() - check(before == artifacts(), "unchanged build reuses Vault artifacts") - - theorem_starts = [ - (match.group(1), line_no) - for line_no, line in enumerate(proof_text.splitlines(), 1) - if (match := re.match(r"theorem\s+([A-Za-z0-9_']+)", line.strip())) - ] - theorem_ranges = { - name: (start, theorem_starts[index + 1][1] - 1 if index + 1 < len(theorem_starts) - else len(proof_text.splitlines())) - for index, (name, start) in enumerate(theorem_starts) - } - - edit_source(original.replace(b"assets", b"depositAmount")) - build() - check(True, "declaration-ID based parameter rename preserves proofs") - edit_source(original) - build() - - def slot_numbers() -> list[str]: - slot_probe = root / ".lake/solidity-import/SlotProbe.lean" - try: - slot_probe.write_text( - "import Contracts.VaultFromSolidity.VaultFromSolidity\n" - "#eval [Contracts.VaultFromSolidity.totalAssetsSlot.slot," - " Contracts.VaultFromSolidity.totalSupplySlot.slot," - " Contracts.VaultFromSolidity.shareBalancesSlot.slot]\n" - ) - return re.findall(r"\[\d+, \d+, \d+\]", run(root, [LAKE, "env", "lean", str(slot_probe)])) - finally: - slot_probe.unlink(missing_ok=True) - - declarations = b" uint256 public totalAssets;\n uint256 public totalSupply;\n" - check(original.count(declarations) == 1 and slot_numbers() == ["[0, 1, 2]"], - "baseline storage layout puts totalAssets, totalSupply, shareBalances at slots 0, 1, 2") - edit_source(original.replace( - declarations, b" uint256 public totalSupply;\n uint256 public totalAssets;\n")) - build() - check(slot_numbers() == ["[1, 0, 2]"], - "reordering state variables binds the spec names to the new slots and proofs still pass") - edit_source(original) - build() - - edit_source(original.replace(b"totalAssets", b"assetsTotal")) - output = build(False, "Invalid field `totalAssets`") - check(re.search(r"Contracts/VaultFromSolidity/Spec\.lean:\d+:\d+:", output) is not None, - "renaming a state variable makes the named spec fail to elaborate") - edit_source(original) - build() - - def broken_theorems(output: str) -> set[str]: - error_lines = [int(value) for value in re.findall( - r"Contracts/VaultFromSolidity/Proofs/ExecutionProof\.lean:(\d+):", output)] - return {name for name, (start, end) in theorem_ranges.items() - if any(start <= line <= end for line in error_lines)} - - # A behaviour change in Vault.sol must break both the success theorem - # and the human-facing `*_meets_spec` theorem. Lean adds a failed - # theorem with `sorry`, so a spec theorem derived from the success - # lemma would keep elaborating; requiring an error inside its own range - # shows the spec layer is proved against the imported definitions itself. - mint = b" function mint(uint256 a) external { totalSupply += a; }\n" - old_balance = b" function balanceOf(address account) external view returns (uint256) {" - for name, theorems, old, new in ( - ("deposit behavior", ("deposit_success_spec", "deposit_meets_spec"), - b"totalSupply += assets;", b"totalSupply = assets;"), - ("getter behavior", ("balance_success_spec", "balance_meets_spec"), - b"return shareBalances[account];", b"return totalAssets;"), - ("new entry point", ("solvent_invariant",), old_balance, mint + old_balance), - ): - check(original.count(old) == 1, name + " mutation has one source target") - before = artifacts() - edit_source(original.replace(old, new)) - output = build(False, "Contracts.VaultFromSolidity.Proofs.ExecutionProof") - broken = broken_theorems(output) - for theorem in theorems: - check(theorem in broken, name + f" mutation breaks {theorem}") - check(before != artifacts(), name + " preserved-mtime edit refreshes artifacts") - edit_source(original) - build() - - - # The spec layer is not vacuous: a wrong promise in Spec.lean is unprovable - # against the unchanged Solidity, and the failure lands in both the - # corresponding success theorem and the `*_meets_spec` theorem. - spec = root / "Contracts/VaultFromSolidity/Spec.lean" - spec_original = spec.read_bytes() - for name, theorems, old, new in ( - ("deposit spec without the supply increment", - ("deposit_success_spec", "deposit_meets_spec"), - b"post.totalSupply = pre.totalSupply + amount", b"post.totalSupply = pre.totalSupply"), - ("withdraw spec without the supply decrement", - ("withdraw_success_spec", "withdraw_meets_spec"), - b"post.totalSupply = pre.totalSupply - amount", b"post.totalSupply = pre.totalSupply"), - ("getter spec returning the wrong variable", - ("balance_success_spec", "balance_meets_spec"), - b"result = v.shareBalances account", b"result = v.totalSupply"), - ): - check(spec_original.count(old) == 1, name + " mutation has one spec target") - spec.write_bytes(spec_original.replace(old, new)) - try: - output = build(False, "Contracts.VaultFromSolidity.Proofs.ExecutionProof") - finally: - spec.write_bytes(spec_original) - broken = broken_theorems(output) - for theorem in theorems: - check(theorem in broken, name + f" is unprovable: breaks {theorem}") - build() - - for name, old, new, diagnostic in ( - ("contract layout at", b"contract Vault {", b"contract Vault layout at 100 {", "layout at"), - ("initializer", b"uint256 public totalAssets;", b"uint256 public totalAssets = 1;", "initializer"), - ("unchecked block", b"totalAssets += assets;", b"unchecked { totalAssets += assets; }", "UncheckedBlock"), - ("loop", b"totalAssets += assets;", b"while (assets < totalAssets) { totalAssets += assets; }", "WhileStatement"), - ("second contract", b"contract Vault {", b"contract Other {}\ncontract Vault {", "exactly one"), - ("multiplication", b"totalAssets += assets;", b"totalAssets = totalAssets * assets;", "unsupported binary"), - ("reserved Storage name", b"uint256 public totalSupply;", - b"uint256 public totalSupply;\n uint256 public Storage;", "unsupported/reserved name"), - ("reserved step name", b"uint256 public totalSupply;", - b"uint256 public totalSupply;\n uint256 public step;", "unsupported/reserved name"), - ): - edit_source(original.replace(old, new)) - output = build(False, diagnostic) - check(re.search(r"Contracts/VaultFromSolidity/Vault.sol:\d+:\d+:", output) is not None, - name + " rejected with source position") - edit_source(original) - build() - - with tempfile.TemporaryDirectory(prefix="verity-vault-outside-", dir=ROOT.parent) as outside: - escaped = Path(outside) / "Vault.sol" - escaped.write_bytes(original) - source.unlink() - source.symlink_to(escaped) - try: - probe.write_text('''import Contracts.VaultFromSolidity.Importer.Importer -solidity_contract Escaped from "../../Contracts/VaultFromSolidity/Vault.sol" -''') - run(root, [LAKE, "env", "lean", str(probe)], False, "source outside package") - check(True, "registered-source symlink escape rejected") - finally: - probe.unlink(missing_ok=True) - source.unlink() - edit_source(original) - build() - - before = artifacts() - digest_before = source_digest() - importer.write_bytes(importer_original + b"\n-- acceptance translation identity probe\n") - os.utime(importer, ns=(importer_stamp.st_atime_ns, importer_stamp.st_mtime_ns)) - build() - check(before != artifacts(), "Lean importer content change invalidates Vault artifacts") - check(digest_before != source_digest(), "Lean importer content changes sourceDigest") - importer.write_bytes(importer_original) - build() - - # The semantics is part of the trusted path, so its content must move the - # digest exactly like the parser does. - semantics = root / "Contracts/VaultFromSolidity/Importer/Semantics.lean" - semantics_original = semantics.read_bytes() - semantics_stamp = semantics.stat() - before = artifacts() - digest_before = source_digest() - semantics.write_bytes(semantics_original + b"\n-- acceptance semantics identity probe\n") - os.utime(semantics, ns=(semantics_stamp.st_atime_ns, semantics_stamp.st_mtime_ns)) - build() - check(before != artifacts(), "Lean semantics content change invalidates Vault artifacts") - check(digest_before != source_digest(), "Lean semantics content changes sourceDigest") - semantics.write_bytes(semantics_original) - build() - - policy = root / "lakefile.lean" - policy_original = policy.read_bytes() - before = artifacts() - policy.write_bytes(policy_original + b"\n-- acceptance build-policy probe\n") - build() - check(before != artifacts(), "build-policy content change invalidates Vault artifacts") - policy.write_bytes(policy_original) - build() - - compiler_stamp = compiler.stat() - compiler.write_bytes(compiler_original + b"\nacceptance-check\n") - os.utime(compiler, ns=(compiler_stamp.st_atime_ns, compiler_stamp.st_mtime_ns)) - build(False, "compiler checksum mismatch") - check(True, "compiler content mutation fails closed") - compiler.write_bytes(compiler_original) - build() - - # Synthetic compiler-output probes test the JSON boundary itself. The - # temporary wrapper is checksummed and accepted only in this disposable - # package; production still executes the pinned binary directly. - real_compiler = compiler.with_name("solc-real") - pin = SOLC_PIN.encode() - for mode, diagnostic in ( - ("ast", "unexpected AST fields"), - ("metadata", "unexpected AST fields"), - ("typed", "invalid AST metadata field Assignment.isLValue"), - ("missing", "missing AST fields"), - ("span", "source span outside registered source"), - ("layout", "missing/packed layout"), - ): - try: - real_compiler.write_bytes(compiler_original) - real_compiler.chmod(0o755) - compiler.write_text(f'''#!/usr/bin/env python3 -import json, pathlib, subprocess, sys -real = pathlib.Path(__file__).with_name("solc-real") -p = subprocess.run([str(real), *sys.argv[1:]], input=sys.stdin.buffer.read(), capture_output=True) -if "--standard-json" not in sys.argv: - sys.stdout.buffer.write(p.stdout); sys.stderr.buffer.write(p.stderr); raise SystemExit(p.returncode) -o = json.loads(p.stdout) -if {mode!r} in ("ast", "metadata", "typed", "missing", "span"): - def mutate(x): - if isinstance(x, dict): - if {mode!r} == "ast" and x.get("nodeType") == "Assignment": - x["unknownExecutableField"] = True; return True - if {mode!r} == "metadata" and x.get("nodeType") == "StructuredDocumentation": - x["unexpectedExecutable"] = {{"nodeType": "UncheckedBlock", "id": 999999, "src": "0:0:0"}} - return True - if {mode!r} == "typed" and x.get("nodeType") == "Assignment": - x["isLValue"] = {{"nodeType": "UncheckedBlock", "id": 999999, "src": "0:0:0"}} - return True - if {mode!r} == "missing" and x.get("nodeType") == "Assignment": - del x["isPure"] - return True - if {mode!r} == "span" and x.get("nodeType") == "Assignment": - start, size, _ = x["src"].split(":") - x["src"] = f"{{start}}:{{size}}:999" - return True - return any(mutate(v) for v in x.values()) - if isinstance(x, list): return any(mutate(v) for v in x) - return False - assert mutate(o) -else: - o["contracts"]["Contracts/VaultFromSolidity/Vault.sol"]["Vault"]["storageLayout"]["storage"][0]["offset"] = 1 -sys.stdout.write(json.dumps(o)) -''') - compiler.chmod(0o755) - wrapper_hash = hashlib.sha256(compiler.read_bytes()).hexdigest().encode() - check(pin in importer_original, "compiler pin occurs in Lean importer") - importer.write_bytes(importer_original.replace(pin, wrapper_hash)) - run(root, [LAKE, "build", "VaultSolidityImporter"]) - build(False, diagnostic) - check(True, f"synthetic {mode} compiler output fails closed") - finally: - real_compiler.unlink(missing_ok=True) - compiler.write_bytes(compiler_original) - compiler.chmod(0o755) - importer.write_bytes(importer_original) - build() - - # Corrupt a late declaration's type. Synchronous checking must reject it - # before any declaration from the failed namespace escapes the transaction. - try: - importer.write_bytes(importer_original.replace( - b" addDecl (.defnDecl { name, levelParams := [], type, value, hints := .regular 0, safety := .safe })", - b' let type := if name.toString.endsWith ".deposit" then mkConst ``Nat else type\n' - b" addDecl (.defnDecl { name, levelParams := [], type, value, hints := .regular 0, safety := .safe })", - )) - run(root, [LAKE, "build", "VaultSolidityImporter"]) - probe.write_text('''import Contracts.VaultFromSolidity.Importer.Importer -open Lean Elab Command -set_option Elab.async true -run_cmd do - let mut rejected := false - try - SolidityImporter.elabSolidityContract - (← `(command| solidity_contract $(mkIdent `Broken):ident from "../../Contracts/VaultFromSolidity/Vault.sol")) - catch e => - rejected := true - logInfo m!"EXPECTED_KERNEL_ERROR {e.toMessageData}" - unless rejected do throwError "malformed declaration accepted" - for suffix in ["totalAssetsSlot", "totalSupplySlot", "shareBalancesSlot", - "totalAssets", "totalSupply", "shareBalances", "deposit", "sourceDigest", - "Storage", "Storage.mk", "Storage.totalAssets", "Storage.totalSupply", - "Storage.shareBalances", "view", "step"] do - if (← getEnv).contains (`Broken ++ suffix.toName) then - throwError "partial declaration escaped rollback: {suffix}" - logInfo "KERNEL_REJECTION_ROLLED_BACK" -''') - output = run(root, [LAKE, "env", "lean", str(probe)]) - check("KERNEL_REJECTION_ROLLED_BACK" in output and "(kernel)" in output, - "malformed late declaration rejected synchronously with full rollback") - finally: - probe.unlink(missing_ok=True) - importer.write_bytes(importer_original) - build() - - check(set(root.rglob("*.lean")) == lean_sources, - "no generated model .lean files") - check(source.read_bytes() == original and importer.read_bytes() == importer_original, - "temporary mutations restored; final baseline passes") - print(f"PASS all Lean-only Vault acceptance checks ({len(theorem_names)} audited theorems)", flush=True) - - -if __name__ == "__main__": - main() diff --git a/Contracts/VaultFromSolidity/Proofs/ExecutionProof.lean b/Contracts/VaultFromSolidity/Proofs/ExecutionProof.lean deleted file mode 100644 index 7ed8a8b933..0000000000 --- a/Contracts/VaultFromSolidity/Proofs/ExecutionProof.lean +++ /dev/null @@ -1,153 +0,0 @@ -import Contracts.VaultFromSolidity.Spec -import Verity.Proofs.Stdlib.SolidityImport - -/-! -# Proofs about the Vault imported from Solidity - -Three layers, deliberately kept small: - -1. `*_success_spec` -- each successful entry point produces a post-state (and, - for `balanceOf`, a return value) that satisfies the named-storage spec. - These unfold the definitions `Importer.lean` registered from `Vault.sol`, so - they fail if the Solidity source changes behaviour. Reverting calls are - excluded by the success hypothesis. -2. `*_meets_spec` -- the named-storage promise from `Spec` under its - precondition. Each states that the call succeeds and that the successful - post-state (or, for `balanceOf`, the returned value) satisfies the spec. -3. `solvent_invariant` -- the contract-level result: `solvent` is preserved by - the importer-generated `step` relation covering every public entry point. - A reverting call leaves the state untouched (`Contract.run` rolls back; - theorem `run_snd_cases`), so solvency can only ever be lost on a successful - call, which is what the success theorems and this invariant rule out. - -Slots are only ever referenced through the imported `Slot` handles, so the -proofs do not depend on the storage-layout order solc picks. `view` constructs -a real `Storage` structure from those handles. --/ - -namespace Contracts.VaultFromSolidity.Proofs.ExecutionProof -open Verity -open Verity.Stdlib.Math -open Verity.Core.Invariant -open Verity.Proofs.Stdlib.SolidityImport -open Spec - -/-! ## Successful calls meet the named-storage spec -/ - -theorem deposit_success_spec (s post : ContractState) (amount : Uint256) - (h : (deposit amount).run s = .success () post) : - deposit_spec amount s.sender (view s) (view post) := by - unfold deposit_spec - by_cases h0 : s.msgValue = 0 - · by_cases hbal : ((view s).shareBalances s.sender).val + amount.val ≤ Verity.Core.MAX_UINT256 - · by_cases hat : (view s).totalAssets.val + amount.val ≤ Verity.Core.MAX_UINT256 - · by_cases hsup : (view s).totalSupply.val + amount.val ≤ Verity.Core.MAX_UINT256 - · solidity_simp - subst h - solidity_simp - · solidity_simp - · solidity_simp - · solidity_simp - · solidity_simp - -theorem withdraw_success_spec (s post : ContractState) (amount : Uint256) - (h : (withdraw amount).run s = .success () post) : - withdraw_spec amount s.sender (view s) (view post) := by - unfold withdraw_spec - by_cases h0 : s.msgValue = 0 - · by_cases hbal : amount.val ≤ ((view s).shareBalances s.sender).val - · by_cases hat : amount.val ≤ (view s).totalAssets.val - · by_cases hsup : amount.val ≤ (view s).totalSupply.val - · solidity_simp - subst h - solidity_simp - · solidity_simp - · solidity_simp - · solidity_simp - · solidity_simp - -theorem balance_success_spec (s post : ContractState) (account : Address) (r : Uint256) - (h : (balanceOf account).run s = .success r post) : - balanceOf_spec account r (view s) ∧ post = s := by - unfold balanceOf_spec - by_cases h0 : s.msgValue = 0 - · solidity_simp - have hr := h.1 - have heq := h.2 - subst heq - exact Eq.symm hr - · solidity_simp - -/-! ## Each entry point meets its named-storage spec -/ - -/-- `balanceOf` succeeds, leaves the state untouched, and returns the -account's shares as `balanceOf_spec` promises. -/ -theorem balance_meets_spec (s : ContractState) (account : Address) - (h0 : s.msgValue = 0) : - ∃ result, (balanceOf account).run s = ContractResult.success result s ∧ - balanceOf_spec account result (view s) := by - unfold balanceOf_spec - solidity_simp - -/-- Under `depositFits`, `deposit` succeeds and its post-state satisfies -`deposit_spec`. -/ -theorem deposit_meets_spec (s : ContractState) (amount : Uint256) - (h0 : s.msgValue = 0) (hfits : depositFits amount s.sender (view s)) : - ∃ post, (deposit amount).run s = ContractResult.success () post ∧ - deposit_spec amount s.sender (view s) (view post) := by - unfold deposit_spec - solidity_simp [depositFits] - -/-- Under `withdrawCovered`, `withdraw` succeeds and its post-state satisfies -`withdraw_spec`. -/ -theorem withdraw_meets_spec (s : ContractState) (amount : Uint256) - (h0 : s.msgValue = 0) (hcovered : withdrawCovered amount s.sender (view s)) : - ∃ post, (withdraw amount).run s = ContractResult.success () post ∧ - withdraw_spec amount s.sender (view s) (view post) := by - unfold withdraw_spec - solidity_simp [withdrawCovered] - -/-! ## The vault stays solvent -/ - -/-- Every public entry point preserves one-for-one backing of issued shares. -The six `rcases` disjuncts are the importer-generated `step` constructors in -source order (functions, then public getters), so a new entry point breaks this -theorem. Rollback is covered by `run_snd_cases`. -/ -theorem solvent_invariant : PreservedBy (fun s => solvent (view s)) step := by - intro s s' hsolvent hstep - rcases hstep with hdep | hwd | hbal | hta | hts | hsb - · rcases hdep with ⟨a, rfl⟩ - cases run_snd_cases (deposit a) s with - | inl hrev => rw [hrev]; exact hsolvent - | inr hsucc => - rcases hsucc with ⟨_, post, hrun, hsnd⟩ - rw [hsnd] - have hspec := deposit_success_spec s post a hrun - unfold solvent at hsolvent ⊢ - rcases hspec with ⟨hassets, hsupply, _⟩ - rw [hassets, hsupply, hsolvent] - · rcases hwd with ⟨a, rfl⟩ - cases run_snd_cases (withdraw a) s with - | inl hrev => rw [hrev]; exact hsolvent - | inr hsucc => - rcases hsucc with ⟨_, post, hrun, hsnd⟩ - rw [hsnd] - have hspec := withdraw_success_spec s post a hrun - unfold solvent at hsolvent ⊢ - rcases hspec with ⟨hassets, hsupply, _⟩ - rw [hassets, hsupply, hsolvent] - · rcases hbal with ⟨k, rfl⟩ - cases run_snd_cases (balanceOf k) s with - | inl hrev => rw [hrev]; exact hsolvent - | inr hsucc => - rcases hsucc with ⟨r, post, hrun, hsnd⟩ - rw [hsnd, (balance_success_spec s post k r hrun).2] - exact hsolvent - · subst hta - by_cases h0 : s.msgValue = 0 <;> solidity_simp [solvent] - · subst hts - by_cases h0 : s.msgValue = 0 <;> solidity_simp [solvent] - · rcases hsb with ⟨k, rfl⟩ - by_cases h0 : s.msgValue = 0 <;> solidity_simp [solvent] - -end Contracts.VaultFromSolidity.Proofs.ExecutionProof diff --git a/Contracts/VaultFromSolidity/Spec.lean b/Contracts/VaultFromSolidity/Spec.lean deleted file mode 100644 index 426f5cb97c..0000000000 --- a/Contracts/VaultFromSolidity/Spec.lean +++ /dev/null @@ -1,68 +0,0 @@ -import Verity.EVM.Uint256 -import Contracts.VaultFromSolidity.VaultFromSolidity - -/-! -# What the imported Vault is supposed to do - -`Importer.lean` turns `Vault.sol` into ordinary Verity definitions: `deposit`, -`withdraw`, `balanceOf`, and a read-only storage view named after the Solidity -state variables. For `v : Storage`, `v.totalAssets`, `v.totalSupply` and -`v.shareBalances` read those variables; `view s` is the view of a state `s`. -The storage-layout slot behind each name comes from solc, not from this file, -so reordering the Solidity declarations does not change anything here. - -This file only states the promise: - -* `solvent` -- the property that matters for the contract as a whole. Shares are - issued one-for-one against assets, so every share outstanding must stay backed - by an asset the vault accounts for. -* `deposit_spec` / `withdraw_spec` / `balanceOf_spec` -- what each entry point - does to the named storage, under the preconditions `depositFits` / - `withdrawCovered`. --/ - -namespace Contracts.VaultFromSolidity.Spec - -open Verity -open Verity.EVM.Uint256 - -/-- The vault's main invariant: issued shares are exactly backed by assets. -If this ever breaks, shares stop being redeemable one-for-one and the vault is -insolvent. -/ -def solvent (v : Storage) : Prop := - v.totalAssets = v.totalSupply - -/-- `deposit(amount)` credits the caller's shares and both totals by `amount`, -and leaves every other account's shares alone. -/ -def deposit_spec (amount : Uint256) (caller : Address) (pre post : Storage) : Prop := - post.totalAssets = pre.totalAssets + amount ∧ - post.totalSupply = pre.totalSupply + amount ∧ - post.shareBalances caller = pre.shareBalances caller + amount ∧ - ∀ other, other ≠ caller → post.shareBalances other = pre.shareBalances other - -/-- `withdraw(amount)` debits the caller's shares and both totals by `amount`, -and leaves every other account's shares alone. -/ -def withdraw_spec (amount : Uint256) (caller : Address) (pre post : Storage) : Prop := - post.totalAssets = pre.totalAssets - amount ∧ - post.totalSupply = pre.totalSupply - amount ∧ - post.shareBalances caller = pre.shareBalances caller - amount ∧ - ∀ other, other ≠ caller → post.shareBalances other = pre.shareBalances other - -/-- `balanceOf(account)` returns the account's shares. -/ -def balanceOf_spec (account : Address) (result : Uint256) (v : Storage) : Prop := - result = v.shareBalances account - -/-- A deposit of `amount` overflows none of the three counters it increments. -/ -def depositFits (amount : Uint256) (caller : Address) (v : Storage) : Prop := - (v.shareBalances caller).val + amount.val ≤ Verity.Core.MAX_UINT256 ∧ - v.totalAssets.val + amount.val ≤ Verity.Core.MAX_UINT256 ∧ - v.totalSupply.val + amount.val ≤ Verity.Core.MAX_UINT256 - -/-- A withdrawal of `amount` is covered by the caller's shares and both totals, -so none of the three guards in `withdraw` reverts. -/ -def withdrawCovered (amount : Uint256) (caller : Address) (v : Storage) : Prop := - amount.val ≤ (v.shareBalances caller).val ∧ - amount.val ≤ v.totalAssets.val ∧ - amount.val ≤ v.totalSupply.val - -end Contracts.VaultFromSolidity.Spec diff --git a/Contracts/VaultFromSolidity/Vault.sol b/Contracts/VaultFromSolidity/Vault.sol deleted file mode 100644 index 577aa14120..0000000000 --- a/Contracts/VaultFromSolidity/Vault.sol +++ /dev/null @@ -1,42 +0,0 @@ -// SPDX-License-Identifier: MIT -pragma solidity ^0.8.33; - -/// @title Vault -/// @notice Minimal ERC4626-style vault with 1:1 asset/share accounting. -/// @dev Source contract for the proof-only Solidity importer example. -contract Vault { - uint256 public totalAssets; - uint256 public totalSupply; - mapping(address => uint256) public shareBalances; - - error InsufficientShares(); - error InsufficientAssets(); - error InsufficientSupply(); - - function deposit(uint256 assets) external { - shareBalances[msg.sender] += assets; - totalAssets += assets; - totalSupply += assets; - } - - function withdraw(uint256 shares) external { - uint256 currentShares = shareBalances[msg.sender]; - if (currentShares < shares) { - revert InsufficientShares(); - } - if (totalAssets < shares) { - revert InsufficientAssets(); - } - if (totalSupply < shares) { - revert InsufficientSupply(); - } - - shareBalances[msg.sender] = currentShares - shares; - totalAssets -= shares; - totalSupply -= shares; - } - - function balanceOf(address account) external view returns (uint256) { - return shareBalances[account]; - } -} diff --git a/Contracts/VaultFromSolidity/VaultFromSolidity.lean b/Contracts/VaultFromSolidity/VaultFromSolidity.lean deleted file mode 100644 index b0050628c1..0000000000 --- a/Contracts/VaultFromSolidity/VaultFromSolidity.lean +++ /dev/null @@ -1,7 +0,0 @@ -import Contracts.VaultFromSolidity.Importer.Importer - -namespace Contracts - -solidity_contract VaultFromSolidity from "Vault.sol" - -end Contracts diff --git a/Makefile b/Makefile index b58b4b55fc..6ef84e4faf 100644 --- a/Makefile +++ b/Makefile @@ -3,7 +3,7 @@ # Prerequisites: curl, git, python3, bash # Run `make setup` to install all tooling, then `make verify` to check all proofs. -.PHONY: help setup setup-elan setup-solc setup-solc-importer check-solc-published setup-foundry \ +.PHONY: help setup setup-elan setup-solc check-solc-published setup-foundry \ verify verify-packages verify-targeted profile-lean test test-foundry test-python axiom-report \ compile generate-yul check checks test-evmyullean-fork \ refresh-status all clean @@ -23,7 +23,7 @@ help: ## Show this help # Setup # --------------------------------------------------------------------------- -setup: setup-elan setup-solc setup-solc-importer setup-foundry ## Install all tooling (elan, solc, foundry) +setup: setup-elan setup-solc setup-foundry ## Install all tooling (elan, solc, foundry) @echo "" @echo "Setup complete. Run 'make verify' to check all proofs." @@ -49,9 +49,6 @@ setup-solc: ## Install solc (SHA256-verified) echo "solc $(SOLC_VERSION) installed"; \ fi -setup-solc-importer: ## Install official solc for the Lean Solidity importer - python3 scripts/setup_solc_importer.py - check-solc-published: ## Fetch official list.json and verify committed SHA-256 pins python3 scripts/check_solc_pin.py --verify-published-checksums @@ -171,7 +168,6 @@ check: ## Run local CI-equivalent checks job (no Lean build, no solc) python3 scripts/generate_print_axioms.py --check python3 scripts/generate_trust_surface_report.py --check python3 scripts/lean_lint.py --only proof_length - python3 scripts/lean_lint.py --only spec_named_storage python3 scripts/check_issue_1060_integrity.py python3 scripts/update_doc_numbers.py --check python3 -m unittest discover -s scripts -p 'test_*.py' -v diff --git a/PrintAxioms.lean b/PrintAxioms.lean index aa7728b806..94bc97f4c5 100644 --- a/PrintAxioms.lean +++ b/PrintAxioms.lean @@ -4,9 +4,6 @@ import Compiler.Proofs.YulGeneration.Backends.EvmYulLeanNativeStepLemmas import Compiler.CompilationModel.ReservedScratchNames -import Contracts.SolidityImportSmoke.Inheritance.Proofs -import Contracts.SolidityImportSmoke.Modifiers.Proofs -import Contracts.SolidityImportSmoke.Structs.Proofs import Contracts.Counter.Proofs.Basic import Contracts.Counter.Proofs.Correctness import Contracts.Counter.Proofs.Preview @@ -38,7 +35,6 @@ import Contracts.SimpleToken.Proofs.Isolation import Contracts.SimpleToken.Proofs.Supply import Contracts.Vault.Proofs.Correctness import Contracts.Vault.Proofs.Native -import Contracts.VaultFromSolidity.Proofs.ExecutionProof import Verity.Proofs.CheckedExternalCallConsumer import Verity.Proofs.LoopSimulationResultAware import Verity.Proofs.Model.CommonExternalCallEquivalence @@ -47,7 +43,6 @@ import Verity.Proofs.Stdlib.Int256 import Verity.Proofs.Stdlib.ListSum import Verity.Proofs.Stdlib.MappingAutomation import Verity.Proofs.Stdlib.Math -import Verity.Proofs.Stdlib.SolidityImport import Compiler.Proofs.AbiDynamicEventObservable import Compiler.Proofs.AbiEncoding import Compiler.Proofs.AbiEventObservable @@ -296,32 +291,6 @@ end Verity.AxiomAudit Compiler.CompilationModel.compatScratch_startsWith_reserved Compiler.CompilationModel.compatScratch_not_internalImmutable - -- Contracts/SolidityImportSmoke/Inheritance/Proofs.lean - Contracts.SolidityImportSmoke.Inheritance.Proofs.pause_success_spec - Contracts.SolidityImportSmoke.Inheritance.Proofs.go_success_spec - Contracts.SolidityImportSmoke.Inheritance.Proofs.bump_success_spec - Contracts.SolidityImportSmoke.Inheritance.Proofs.dispatch_is_child - Contracts.SolidityImportSmoke.Inheritance.Proofs.super_runs_parent - Contracts.SolidityImportSmoke.Inheritance.Proofs.pause_meets_spec - Contracts.SolidityImportSmoke.Inheritance.Proofs.go_meets_spec - Contracts.SolidityImportSmoke.Inheritance.Proofs.bump_meets_spec - Contracts.SolidityImportSmoke.Inheritance.Proofs.paused_invariant - - -- Contracts/SolidityImportSmoke/Modifiers/Proofs.lean - Contracts.SolidityImportSmoke.Modifiers.Proofs.status_restored - Contracts.SolidityImportSmoke.Modifiers.Proofs.early_meets_spec - Contracts.SolidityImportSmoke.Modifiers.Proofs.guarded_success_spec - Contracts.SolidityImportSmoke.Modifiers.Proofs.guarded_reverts_not_owner_before_pause - Contracts.SolidityImportSmoke.Modifiers.Proofs.tagged_uses_base_helper - Contracts.SolidityImportSmoke.Modifiers.Proofs.snapshot_restores_status - - -- Contracts/SolidityImportSmoke/Structs/Proofs.lean - Contracts.SolidityImportSmoke.Structs.Proofs.set_then_get - Contracts.SolidityImportSmoke.Structs.Proofs.other_member_unchanged_thm - Contracts.SolidityImportSmoke.Structs.Proofs.get_meets_spec - Contracts.SolidityImportSmoke.Structs.Proofs.make_encodes - Contracts.SolidityImportSmoke.Structs.Proofs.makeRev_encodes - -- Contracts/Counter/Proofs/Basic.lean Contracts.Counter.Proofs.setStorage_updates_count Contracts.Counter.Proofs.getStorage_reads_count @@ -721,15 +690,6 @@ end Verity.AxiomAudit Contracts.Vault.Proofs.Native.vaultMinimal_runtime_lowers_native Contracts.Vault.Proofs.Native.vaultMinimal_totalAssets_nativeResultsMatchOn_revert_of_nonzero_value - -- Contracts/VaultFromSolidity/Proofs/ExecutionProof.lean - Contracts.VaultFromSolidity.Proofs.ExecutionProof.deposit_success_spec - Contracts.VaultFromSolidity.Proofs.ExecutionProof.withdraw_success_spec - Contracts.VaultFromSolidity.Proofs.ExecutionProof.balance_success_spec - Contracts.VaultFromSolidity.Proofs.ExecutionProof.balance_meets_spec - Contracts.VaultFromSolidity.Proofs.ExecutionProof.deposit_meets_spec - Contracts.VaultFromSolidity.Proofs.ExecutionProof.withdraw_meets_spec - Contracts.VaultFromSolidity.Proofs.ExecutionProof.solvent_invariant - -- Verity/Proofs/CheckedExternalCallConsumer.lean Verity.Proofs.CheckedExternalCallConsumer.lido_submit_entry_installs_caller_context Verity.Proofs.CheckedExternalCallConsumer.lido_submit_success_world @@ -1190,10 +1150,6 @@ end Verity.AxiomAudit Verity.Proofs.Stdlib.Math.safeMul_result_bounded Verity.Proofs.Stdlib.Math.safeDiv_result_le_numerator - -- Verity/Proofs/Stdlib/SolidityImport.lean - Verity.Proofs.Stdlib.SolidityImport.run_snd_cases - Verity.Proofs.Stdlib.SolidityImport.uint256_eq_zero_iff - -- Compiler/Proofs/AbiDynamicEventObservable.lean Compiler.Proofs.AbiDynamicEventObservable.evalIRExprs_append Compiler.Proofs.AbiDynamicEventObservable.evalIRExprs_map_normalizeEventWord @@ -7626,4 +7582,4 @@ end Verity.AxiomAudit Compiler.Proofs.YulGeneration.YulTransaction.ofIR_args ] --- Total: 7040 theorems/lemmas (5027 public, 2013 private, 0 sorry'd) +-- Total: 7011 theorems/lemmas (4998 public, 2013 private, 0 sorry'd) diff --git a/README.md b/README.md index 6694d4e27b..8fa5d83c3d 100644 --- a/README.md +++ b/README.md @@ -21,82 +21,6 @@ **Verity** is a formally verified smart contract compiler written in [Lean 4](https://lean-lang.org/). You write contracts in an embedded DSL, state what they should do, prove those properties hold, and compile to EVM bytecode. The compiler itself is proven to preserve semantics across three verified layers. Full documentation lives at [**veritylang.com**](https://veritylang.com). -## Proof-only Solidity Vault import (POC) - -`Contracts/VaultFromSolidity/VaultFromSolidity.lean` imports the colocated -`Vault.sol` with `solidity_contract VaultFromSolidity from "Vault.sol"`. -The Lean frontend invokes pinned solc 0.8.33 for typed AST and storage layout, -validates them, and parses them into the closed, intrinsically typed inductive in -`Contracts/VaultFromSolidity/Importer/Syntax.lean`. `Importer/Semantics.lean` -gives each construct its one meaning, and each entry point is registered in -memory as `Fn.meaning` applied to its parsed term, as a transparent, -kernel-checked `Verity.Contract` definition. There is no Python -frontend, custom serialized IR, generated `.lean`, CompilationModel, or -bytecode. The example is independent of the -handwritten `Contracts/Vault` contract. - -The importer elaborates a kernel-checked `Storage` structure named after the -Solidity state variables, so `Spec.lean` reads like the contract instead of -naming raw slots: - -```lean --- before -def solvent (s : ContractState) : Prop := s.readSlot 0 = s.readSlot 1 - --- after -def solvent (v : Storage) : Prop := v.totalAssets = v.totalSupply - -def deposit_spec (amount : Uint256) (caller : Address) (pre post : Storage) : Prop := - post.totalAssets = pre.totalAssets + amount ∧ - post.totalSupply = pre.totalSupply + amount ∧ - post.shareBalances caller = pre.shareBalances caller + amount ∧ - ∀ other, other ≠ caller → post.shareBalances other = pre.shareBalances other -``` - -`view` constructs that `Storage` from the `Slot` handles solc's storage -layout produced, so reordering the Solidity declarations moves the slots without -touching the spec, and renaming a variable makes the spec fail to elaborate. -`#print view` shows the `readSlot`/`readMap` unfolding. The importer also -registers a deterministic entry-point relation `step`. `Proofs/ExecutionProof.lean` -proves each successful call meets its spec and that `solvent` is preserved by -`step` (`solvent_invariant`). - -With the Lean/package prerequisites installed, put an official solc 0.8.33 -build at `.lake/solidity-import/solc` (`make setup-solc-importer` fetches the -host platform's `list.json` from binaries.soliditylang.org, checks the -published SHA-256 against the committed pin, and installs that binary). The -importer allowlists the official linux-amd64 and macosx-amd64 digests; Linux -CI still uses linux-amd64. Then run: - -```sh -lake build VaultFromSolidity -python3 Contracts/VaultFromSolidity/Importer/scripts/solidity_importer_test.py -lake build SolidityImportSmokeInheritance -python3 Contracts/SolidityImportSmoke/Inheritance/scripts/inheritance_test.py -lake build SolidityImportSmokeModifiers -python3 Contracts/SolidityImportSmoke/Modifiers/scripts/modifiers_test.py -lake build SolidityImportSmokeStructs -python3 Contracts/SolidityImportSmoke/Structs/scripts/structs_test.py -``` - -The Vault acceptance script uses disposable copies for source mutations, fail-closed -rejection, content-based Lake freshness, compiler/importer/build-policy -invalidation, declaration-registration rollback, and an audit of every Vault -theorem. It never mutates the original Solidity file. The inheritance smoke -(`Contracts/SolidityImportSmoke/Inheritance`) covers same-file `is` bases, C3 -linearization including a diamond, virtual dispatch, `super` (target C3, not -the defining-contract AST id), opaque fields, and internal calls (`Expr.call` -is view/pure only); `inheritance_test.py` is the matching focused suite. The -modifiers smoke inlines argument-free modifiers in declaration order, including -`nonReentrant` postludes after an early `return` (`super` in a modifier uses -the modifier's defining contract; a prelude `return` before `_` is rejected). -The structs smoke encodes and decodes a two-member user struct used as a -parameter, storage field, and return (canonical names, one-shot RHS, zero -defaults, public product getter, struct ids in signatures). -Save Solidity, rebuild this dedicated target, then reload the Lean editor: -an already-open editor snapshot does not automatically watch `.sol` changes. -See [the trust boundary](TRUST_ASSUMPTIONS.md#proof-only-solidity-vault-import). - ## Verification status All proofs are machine-checked by the Lean kernel. CI rebuilds the proof development on every commit, and repository checks enforce that no proof is left incomplete (no `sorry`) and that the compiler proof stack carries 0 axioms (see [AXIOMS.md](AXIOMS.md)). Verification is scoped rather than total: the generic compiler theorems cover an explicitly documented fragment of the language, and the precise boundary between what is proven and what is trusted is maintained in [TRUST_ASSUMPTIONS.md](TRUST_ASSUMPTIONS.md). diff --git a/TRUST_ASSUMPTIONS.md b/TRUST_ASSUMPTIONS.md index 5aea81ba54..44a5969c63 100644 --- a/TRUST_ASSUMPTIONS.md +++ b/TRUST_ASSUMPTIONS.md @@ -27,8 +27,7 @@ model matches the Solidity source or solc's bytecode. `storageLayout` slots and packed offsets are copied from pinned solc 0.8.34 (`0.8.34+commit.80d5c536`; linux-amd64 and macosx-amd64 SHA-256 pins in `Import.lean`). `mappingSlot` in the denotation oracle is not Keccak. Numeric -claims are about the values `structMember` reads back. The Vault importer's -solc 0.8.33 pin is unchanged. +claims are about the values `structMember` reads back. Checked `uint256` and narrower unsigned arithmetic is lowered to `Stmt.ite` plus `Stmt.panic`, not to a second interpreter. Explicit `uint128(x)` is truncation @@ -43,130 +42,6 @@ The slice makes no dynamic ABI-head claim: that unrelated denotation extension is not part of this change. Lake does not track the external Solidity reads, so consumers must re-elaborate to verify freshness against a compiled import. -## Proof-only Solidity Vault import - -This POC is separate from the verified compilation pipeline below. It trusts -pinned solc's typed AST/storage layout and the Lean translation in -`Contracts/VaultFromSolidity/Importer/Importer.lean` to preserve Solidity -meaning. Kernel checking establishes well-typed definitions and theorems about -their execution, not a Solidity-to-Verity equivalence theorem. `sourceDigest` -is provenance, not proof of correspondence. It hashes the compiler input, -standard-json output, Lean importer implementation, and the release identity -`0.8.33+commit.64118f21` (not the host binary hash or Darwin/Linux banner), so -Linux and macOS share one translation identity when they produce the same AST. -The host's fixed checksum utility is trusted for compiler-pin checks: -`/usr/bin/sha256sum` on Linux and `/usr/bin/shasum` on macOS. The on-disk -compiler must match a committed allowlist of official -`binaries.soliditylang.org` SHA-256 digests for linux-amd64 and macosx-amd64 -(the latter is a universal Mach-O). `make setup-solc-importer` fetches the -platform `list.json`, requires the published digest to equal that pin, then -downloads the listed build. `make check-solc-published` repeats the live -list.json check without installing. Lake elaboration never fetches. Linux CI -still installs and runs the linux-amd64 artifact. -The digest is checked before version inspection, immediately before compilation, -and again after compilation, so `PATH` substitution and persistent compiler -replacement fail closed. As with all local builds, a concurrently malicious -process with the builder's own filesystem privileges is outside the threat model. -It is not full build identity: transitive Verity semantics, Lean toolchain, and -Lake build policy are tracked separately by normal build dependencies, not this -digest. The recursive closed AST schema rejects unknown fields/node kinds and -contract `layout at`; semantically used type metadata and all storage-layout -records are checked explicitly. Canonical package containment is checked -independently of source registration. - -`Importer.lean` runs pinned solc itself with `--standard-json` and -`--no-import-callback`, parses the typed AST/storage layout, validates the -closed subset, resolves IDs/types/storage slots, and parses it into the closed, -intrinsically typed inductive in `Syntax.lean`. Each entry point is registered -as `Semantics.lean`'s `Fn.meaning` applied to that parsed term, so -`Semantics.lean` is the single definition of what each construct means and is -part of the digest. Declaration -registration disables asynchronous kernel checking inside the transaction, -restores the pre-import environment on failure, parses every body at its typed -return signature (an ill-typed body is unrepresentable), and registers safe -transparent definitions. The same -transaction elaborates `Storage` with the standard structure command (the one -use of that elaborator; kernel-checked, rolled back on failure), then -registers `view : ContractState → Storage` built from the imported -`Slot` handles, tags slot handles, getters, functions, and `view` into -the `solidity_import` simp set, and registers a deterministic entry-point -relation `step` (the target's public/external functions in source order, then -each base in linearization order, then public getters in field order). -`Storage`, `view`, and `step` are reserved Solidity names. Opaque fields are -listed in `opaqueFields`. The importer source manifest is `registeredSources`. The -frontend emits no generated Lean source and keeps -no serialized AST/model cache; the parsed term is a kernel-checked Lean value, -never serialized. `Semantics.lean` tags its definitions into the -`solidity_import` simp set, so `solidity_simp` unfolds `Fn.meaning` down to the -Verity primitives exactly as it did before the split. - -The accepted fragment covers the existing Vault, the S1 inheritance slice, and -the S2 modifiers/structs slice: full-width `uint256` scalars, `address` scalars, -address-to-uint256 mappings and public getters, straight-line reads/writes, -locals, checked addition/subtraction, comparison/custom-error guards (including -`address !=` for `onlyOwner`), same-file `is` bases with solc's C3 -linearization (including diamonds), virtual dispatch and `super` specialized at - import time from the target's `linearizedBaseContracts` (matching 0.8.x runtime; - the AST `referencedDeclaration` on `super` follows the defining contract and is - not the dispatch key on diamonds; `super` inside an inlined modifier starts - after that modifier's defining contract, not the function's), internal function - calls (`Expr.call` is - `view`/`pure` only; effectful internals are `Stmt.callStmt`, because legacy - codegen evaluates those calls before the other operand / `+=` old-read), - abstract bases with body-less `virtual`s, opaque storage fields (slot reserved, - not in `Storage`; a body that reads or writes one is rejected), argument-free - modifiers inlined at parse time in declaration order (`Stmt.seq` prelude plus - `Stmt.block` so a function `return` still runs the postlude; prelude `uint256` - locals scope over the inlined body and postlude; identifier calls in a - modifier body that resolve to a private or non-virtual helper stay bound to - the declaring contract even if a derived contract declares a same-name private - function; a modifier with - two `_`, with arguments, or with a prelude `return` before `_` is rejected; - `Semantics.lean` never sees `_`), named `address`/struct returns defaulting to - the zero address rather than `msg.sender`, and - user structs used as parameters, storage, and returns when they are the closed - `uint256` then `address` pair (members occupy consecutive solc slots; encode is - `Expr.pair` indexed by the solc struct id so same-shape structs stay distinct - in signatures; a public struct field registers one product getter; a storage - struct assignment binds the RHS pair once; named `Acc({...})` constructors - evaluate arguments in source order (pinned 0.8.33) then pack into the - `uint256 × address` member product — `Expr.pair` when names are already - member order, `Expr.pairRev` when they are `{who, amount}`; positional - `Acc(a, b)` stays AST/member order). Unknown executable constructs are -rejected; this is not general Solidity support. Multi-file units, packed -fields, events, and external calls remain out of the fragment. -Arguments/context are already typed and decoded. `Contract.run` rolls back -failed executions; errors are model strings, not verified ABI revert bytes. -The storage model uses logical keys, not a proof of physical keccak layout. -There is no deployment, calldata/dispatch, gas, external interaction, bytecode, -or full EVM equivalence claim. Initial states are arbitrary, not proven deployed -states. Arithmetic success premises restrict the success theorems. The example keeps one -readable proof set: each successful entry point meets its named-storage spec -(`*_success_spec`), each spec holds under its precondition (`*_meets_spec`), -and the vault's solvency invariant (`totalAssets = totalSupply`) is preserved -by `step` (`solvent_invariant`). Rollback on revert is proved by `run_snd_cases`; -the revert conditions themselves are exercised by the acceptance suite, not proved. - -The specification states its promises over the imported storage view -(`v.totalAssets`, `v.shareBalances account`) rather than raw slot numbers; the -execution proof file relates that view to the imported definitions. Each -`*_meets_spec` theorem asserts that the call succeeds under its precondition -and that the successful post-state (or returned value) meets the spec, so a -reverting implementation cannot satisfy it; the `*_success_spec` theorems pin -the same named-storage equations from a success hypothesis. The view adds no -trust: `view` unfolds to `ContractState.readSlot`/`readMap` at the slot -solc's storage layout assigned. Zero-argument custom errors use Verity's `Name()` model convention; -arithmetic panic strings remain a model representation, not an assertion of -matching EVM revert bytes. The statements do not assert full equivalence of all -executions or all public/deployment interfaces. - -Lake's dedicated `VaultFromSolidity` target tracks source/compiler/Lean-importer/build -policy bytes and normal Lean dependencies. Acceptance evidence is obtained with -`python3 Contracts/VaultFromSolidity/Importer/scripts/solidity_importer_test.py`; -that Python file only orchestrates disposable builds and mutations and is not in -the translation path. Stale editor snapshots are not a current-source proof -certificate. No additional project axiom is introduced. - ## Compilation Pipeline ``` diff --git a/Verity/Proofs/Stdlib/SolidityImport.lean b/Verity/Proofs/Stdlib/SolidityImport.lean deleted file mode 100644 index 80827a0da9..0000000000 --- a/Verity/Proofs/Stdlib/SolidityImport.lean +++ /dev/null @@ -1,51 +0,0 @@ -import Lean -import Verity.Core -import Verity.Core.Invariant -import Verity.Core.SolidityImportAttr -import Verity.Stdlib.Math -import Verity.EVM.Uint256 - -namespace Verity.Proofs.Stdlib.SolidityImport - -open Verity -open Lean Parser.Tactic - -/-- `Contract.run` either rolls back to the pre-state or returns a successful post-state. -/ -theorem run_snd_cases {α : Type} (c : Contract α) (s : ContractState) : - (c.run s).snd = s ∨ ∃ a post, c.run s = .success a post ∧ (c.run s).snd = post := by - unfold Contract.run - cases c s - · exact Or.inr ⟨_, _, rfl, rfl⟩ - · exact Or.inl rfl - -/-- The imported nonpayable guard tests `msgValue.val`; this relates it to `= 0`. -/ -theorem uint256_eq_zero_iff (x : Verity.Core.Uint256) : x = 0 ↔ x.val = 0 := by - constructor - · intro h; subst h; exact Verity.Core.Uint256.val_zero - · intro h; exact Verity.Core.Uint256.ext (h.trans Verity.Core.Uint256.val_zero.symm) - -syntax (name := soliditySimpTac) - "solidity_simp" (" [" simpLemma,* "]")? : tactic - -macro_rules - | `(tactic| solidity_simp) => - `(tactic| solidity_simp []) - | `(tactic| solidity_simp [$args,*]) => - `(tactic| simp_all [solidity_import, - $(mkIdent ``Contract.run):ident, $(mkIdent ``Bind.bind):ident, $(mkIdent ``Pure.pure):ident, - $(mkIdent ``Verity.instMonadContract):ident, $(mkIdent ``Verity.bind):ident, $(mkIdent ``Verity.pure):ident, - $(mkIdent ``msgValue):ident, $(mkIdent ``msgSender):ident, $(mkIdent ``Verity.require):ident, - $(mkIdent ``getStorage):ident, $(mkIdent ``setStorage):ident, $(mkIdent ``getStorageAddr):ident, - $(mkIdent ``setStorageAddr):ident, $(mkIdent ``getMapping):ident, - $(mkIdent ``setMapping):ident, $(mkIdent ``Verity.Stdlib.Math.requireSomeUint):ident, - $(mkIdent ``Verity.Stdlib.Math.safeAdd):ident, $(mkIdent ``Verity.Stdlib.Math.safeSub):ident, - $(mkIdent ``Verity.EVM.Uint256.sub):ident, $(mkIdent ``Nat.not_le_of_lt):ident, - $(mkIdent ``Nat.not_lt_of_ge):ident, $(mkIdent ``ContractState.readSlot):ident, - $(mkIdent ``ContractState.writeSlot):ident, $(mkIdent ``ContractState.readAddrSlot):ident, - $(mkIdent ``ContractState.writeAddrSlot):ident, $(mkIdent ``ContractState.readMap):ident, - $(mkIdent ``ContractState.writeMap):ident, $(mkIdent ``ContractState.storage):ident, - $(mkIdent ``ContractState.storageAddr):ident, $(mkIdent ``ContractState.storageMap):ident, - $(mkIdent ``uint256_eq_zero_iff):ident, - $args,*]) - -end Verity.Proofs.Stdlib.SolidityImport diff --git a/artifacts/trust_surface_report.json b/artifacts/trust_surface_report.json index e30ccc2351..589e86c834 100644 --- a/artifacts/trust_surface_report.json +++ b/artifacts/trust_surface_report.json @@ -169,7 +169,7 @@ "mechanisms": { "@[implemented_by": 1, "native_decide": 613, - "partial def": 205 + "partial def": 193 }, "notes": "native_decide trusts Lean.ofReduceBool or Lean 4.31 generated per-proof native_decide axioms + Lean.trustCompiler. Prose registry: AXIOMS.md, TRUST_ASSUMPTIONS.md (enforced by scripts/check_trust_surface_registry.py).", "schema_version": 1 diff --git a/artifacts/verification_status.json b/artifacts/verification_status.json index cdb62b25ee..8ef8e8ed64 100644 --- a/artifacts/verification_status.json +++ b/artifacts/verification_status.json @@ -1,7 +1,7 @@ { "codebase": { "core_lines": 2412, - "example_contracts": 21 + "example_contracts": 19 }, "proofs": { "axioms": 1, @@ -10,16 +10,16 @@ "schema_version": 1, "tests": { "differential_total": 110000, - "foundry_functions": 532, + "foundry_functions": 524, "property_functions": 239, - "suites": 53 + "suites": 52 }, "theorems": { - "categories": 17, - "coverage_percent": 72, + "categories": 15, + "coverage_percent": 78, "covered": 255, - "excluded": 101, - "non_stdlib_total": 356, + "excluded": 74, + "non_stdlib_total": 329, "per_contract": { "Counter": 31, "ERC20": 22, @@ -35,13 +35,11 @@ "SafeCounter": 25, "SimpleStorage": 20, "SimpleToken": 61, - "SolidityImportSmoke": 20, - "Vault": 9, - "VaultFromSolidity": 7 + "Vault": 9 }, - "proven": 356, + "proven": 329, "stdlib": 0, - "total": 356 + "total": 329 }, "toolchain": { "lean": "leanprover/lean4:v4.31.0", diff --git a/docs-site/public/llms.txt b/docs-site/public/llms.txt index c8a39737ff..1c6230e0ce 100644 --- a/docs-site/public/llms.txt +++ b/docs-site/public/llms.txt @@ -31,11 +31,11 @@ Every transition inside the proof envelope is either fully verified or recorded - **Language**: Lean 4.31.0 -- **Core Size**: 2257 lines -- **Verified Contracts**: 17 (Counter, ERC20, ERC721, Ledger, LocalObligationMacroSmoke, Ownable, Owned, OwnedCounter, OwnedCounterComposed, ReentrancyExample, ReentrancyRelyGuarantee, SafeCounter, SimpleStorage, SimpleToken, SolidityImportSmoke, Vault, VaultFromSolidity) -- **Theorems**: 356 across 17 categories, 356 fully proven +- **Core Size**: 2412 lines +- **Verified Contracts**: 15 (Counter, ERC20, ERC721, Ledger, LocalObligationMacroSmoke, Ownable, Owned, OwnedCounter, OwnedCounterComposed, ReentrancyExample, ReentrancyRelyGuarantee, SafeCounter, SimpleStorage, SimpleToken, Vault) +- **Theorems**: 329 across 15 categories, 329 fully proven - **Axioms**: 1 documented Lean axioms (see AXIOMS.md) -- **Tests**: 532 Foundry tests, 239 property tests +- **Tests**: 524 Foundry tests, 239 property tests - **Build**: `lake build` verifies all proofs - **Repository**: https://github.com/lfglabs-dev/verity diff --git a/docs/SOLIDITY_IMPORT_ROADMAP.md b/docs/SOLIDITY_IMPORT_ROADMAP.md deleted file mode 100644 index 09ca837d86..0000000000 --- a/docs/SOLIDITY_IMPORT_ROADMAP.md +++ /dev/null @@ -1,849 +0,0 @@ -# Solidity import roadmap: from the Vault POC to the Pareto Credit Vault - -**Status**: planning document. This file commits to a slice order, the files -each slice touches, the tests and acceptance evidence each slice must ship, -and what each slice explicitly leaves out. It contains no importer code. - -**Goal**: a minimal proof-only Solidity import of `IdleCDOCreditVault`, -`IdleCDOEpochVariant` and `IdleCreditVault` from -[Idle-Labs/idle-tranches @ `54502d1`](https://github.com/Idle-Labs/idle-tranches/tree/54502d1) -together with their OpenZeppelin 4.9.6 upgradeable parents, built by -extending the existing proof-only importer under -[`Contracts/VaultFromSolidity`](../Contracts/VaultFromSolidity). - -**Inputs** - -- POC: [`Contracts/VaultFromSolidity`](../Contracts/VaultFromSolidity) - (`Importer/Importer.lean`, `Importer/Syntax.lean`, - `Importer/Semantics.lean`, `Spec.lean`, `Proofs/ExecutionProof.lean`, - `Importer/scripts/solidity_importer_test.py`). -- Feature list: `audit/VERITY-GAPS.md` in - `lfglabs-dev/pareto-credit-vault-proof-closure` - (branch `assessment/source-faithful-accounting`, read-only), rows G1–G18. -- Existing `verity_contract` support for the same contracts: - [`docs/PARETO_MODEL_SUPPORT.md`](PARETO_MODEL_SUPPORT.md), - [`docs/MODIFIERS_AND_INHERITANCE.md`](MODIFIERS_AND_INHERITANCE.md), - [`docs/EXTERNAL_CALL_MODULES.md`](EXTERNAL_CALL_MODULES.md). - -**Slice order** (fixed): inheritance → modifiers/structs → external calls and -try/catch → signed integers → solc 0.8.10 → one Pareto smoke. - ---- - -## 1. Hard rules for every slice - -These apply to every PR on this roadmap, including the last one. - -1. **No `sorry`, no `admit`, no `native_decide`, no new axioms.** The - axiom audit in `solidity_importer_test.py` (every theorem in every - importer proof file reports only `propext` and `Quot.sound`) is extended - to each new smoke and stays a hard CI gate. External-call behaviour is a - *parameter* of the imported definitions (Section 4.3), never an axiom. - `AXIOMS.md` keeps "Active axioms: 1". -2. **The POC stays green.** `lake build VaultFromSolidity` and the full - `solidity_importer_test.py` run must pass unchanged on every slice. The - seven Vault theorems are the regression baseline for `solidity_simp`. -3. **The accepted subset stays closed and intrinsically typed.** New - constructs are new constructors in `Syntax.lean` with one meaning in - `Semantics.lean`. No generated Lean source, no serialized IR, no - `unsafe`, no `partial` definitions in the semantics. Kernel checking stays - synchronous (`Elab.async := false` inside the import transaction) with - full environment rollback on failure. -4. **Fail closed with a source position.** Every rejection is a - `path:line:col` diagnostic. The AST schema stays an allowlist - (`allowedNodeFields` / `requiredNodeFields`); a new node kind is accepted - only when its meaning is defined. -5. **The digest covers the whole trusted translation.** `sourceDigest` - keeps hashing source bytes, solc output, `Importer.lean`, `Syntax.lean`, - `Semantics.lean`, and the compiler pin. Any new trusted file (for example - a version-indexed schema table) is added to the digest and to the - "content change invalidates artifacts" mutation checks. -6. **Docs move with the boundary.** Each slice updates - `TRUST_ASSUMPTIONS.md` (the "Proof-only Solidity Vault import" section), - `AUDIT.md`, the `AXIOMS.md` audit paragraph, `README.md`'s importer - section, `docs/VERIFICATION_STATUS.md`, `scripts/check_contract_structure.py` - exemptions, and regenerates `PrintAxioms.lean` and - `artifacts/verification_status.json` when theorem counts change. -7. **Git discipline.** One branch per slice from current `origin/main`, one - PR per slice (S2 may be split as noted), never rebase or force-push a - published branch, merge commits from `main` only. Proofs under 30 lines - or allowlisted per `scripts/check_proof_length.py`. - ---- - -## 2. Where we are: the POC - -`solidity_contract Alias from "Vault.sol"` runs pinned solc 0.8.33 -(`--standard-json --no-import-callback`, optimizer off, `evmVersion: cancun`), -validates the typed AST and storage layout against a closed schema, parses -each function body into the intrinsically typed inductive in `Syntax.lean`, -and registers each entry point as `Sol.Fn.meaning body slots env`. It also -elaborates a kernel-checked `Storage` structure, a `view : ContractState → -Storage`, and the entry-point relation `step`. - -Accepted today: - -| Area | Accepted | Rejected (diagnostic) | -|------|----------|-----------------------| -| Source unit | one pragma `^0.8.33`, exactly one non-abstract contract, no bases | imports, several contracts, `abstract`, `is` | -| Storage | `uint256` scalars, `mapping(address => uint256)`, offset 0, 32-byte slots | any other type, packed fields, initializers, constants, `layout at` | -| Declarations | zero-argument custom errors, public getters | events, modifiers, structs, enums, `using for`, constructors | -| Functions | external/public, nonpayable or view, ≤1 parameter (`uint256`/`address`), ≤1 unnamed `uint256` return, no modifiers, not virtual | everything else ("unsupported function surface") | -| Statements | `x = / += / -= e`, `uint256 x = e`, `if (a < b) revert E();`, terminal `return e` | loops, `unchecked`, `if/else`, calls, `emit`, `delete` | -| Expressions | locals, `msg.sender`, literals, state reads, mapping reads, checked `+`/`-` | `*`, `/`, comparisons outside guards, casts, calls, `address(this)` | - -Trust story (`TRUST_ASSUMPTIONS.md`): solc's typed AST/storage layout and the -Lean translation are trusted; kernel checking establishes well-typed -definitions and theorems about their execution; storage keys are logical, -not keccak; `Contract.run` rolls back failed executions; no deployment, -calldata, gas, or bytecode claim. This roadmap keeps that story and only -widens the accepted fragment. - ---- - -## 3. Where we are going: the Pareto sources - -Inheritance chains at `54502d1`: - -``` -IdleCDOEpochVariant - is IdleCDOCreditVault - is PausableUpgradeable, (Initializable, ContextUpgradeable) - GuardedLaunchUpgradable (Initializable, OwnableUpgradeable, ReentrancyGuardUpgradeable) - IdleCDOStorage - -IdleCreditVault - is Initializable, OwnableUpgradeable, ERC20Upgradeable, ReentrancyGuardUpgradeable, IIdleCDOStrategy -``` - -`Initializable` and `ContextUpgradeable` are reached more than once in both -chains: these are real Solidity diamonds resolved by C3 linearization, so the -importer must accept them (unlike `verity_contract is`, which rejects diamonds; -gap G7). - -Files the compiler must see (all vendored, pinned by hash, Section 9): - -- idle-tranches: `IdleCDOCreditVault.sol`, `IdleCDOEpochVariant.sol`, - `strategies/idle/IdleCreditVault.sol`, `GuardedLaunchUpgradable.sol`, - `IdleCDOStorage.sol`, `IdleCDOTranche.sol`, and the interfaces - `IIdleCDOStrategy`, `IERC20Detailed`, `IProgrammableBorrower`, - `keyring/IKeyring`, plus `@uniswap/v2-periphery`'s `IUniswapV2Router02` - (only as the type of an opaque storage field). -- OpenZeppelin contracts-upgradeable 4.9.6: `Initializable`, - `ContextUpgradeable`, `OwnableUpgradeable`, `PausableUpgradeable`, - `ReentrancyGuardUpgradeable`, `ERC20Upgradeable` (+ `IERC20Upgradeable`, - `IERC20MetadataUpgradeable`), `SafeERC20Upgradeable` (+ - `AddressUpgradeable`, `IERC20PermitUpgradeable`). - -Construct inventory (from the three target contracts, their idle-tranches -parents, and the OZ parents), with the slice that owns each construct and the -`VERITY-GAPS.md` row it relates to: - -| Construct | Where it appears | Slice | Gap | -|-----------|------------------|-------|-----| -| `is A, B, C`, C3 linearization, diamonds, `abstract`, body-less `virtual` (`getContractValue`), `override`, `super._transfer` / `super._deposit` | all chains | S1 | G6, G7, G18 | -| internal function calls, internal helpers with several parameters, named return variables | everywhere (`_checkOnlyOwner`, `_virtualPriceAux`, …) | S1 | G13 | -| `uint256[N] __gap`, `address[]`, `bytes32`, `string`, interface-typed fields | OZ parents, `IdleCDOStorage`, `IdleCreditVault` | S1 (opaque fields) | G5, G17 | -| modifiers with prelude only (`onlyOwner`, `whenNotPaused`, `onlyInitializing`) and with postlude (`nonReentrant`, `initializer`) | OZ parents | S2 | G18 | -| `bool`, `uint8`, `address` scalars, packed sub-word fields (`_initialized`/`_initializing`, `isEpochRunning`/`allowAAWithdrawRequest`/…) | OZ, `IdleCDOStorage`, `IdleCDOEpochVariant` | S2 | G4 | -| structs in mappings (`Apr0UserData`), storage pointers to struct members, `delete`, nested mappings `mapping(address => mapping(uint256 => uint256))`, `mapping(uint256 => uint256)` | `IdleCreditVault` | S2 | G2 | -| constants (`FULL_ALLOC`, `YEAR = 365 days`, `10**18`, `type(uint256).max`), `require(cond, "…")`, `if/else`, ternary, `&&`, `\|\|`, `!`, all comparisons, checked `*`, `/`, `%`, `block.timestamp`, `address(this)`, `emit` | everywhere | S2 | G8, G9, G10 | -| typed interface calls on `IERC20Detailed(token)`, `IdleCreditVault(strategy)`, `IIdleCDOEpochVariant(idleCDO)`, `IKeyring`, `IProgrammableBorrower`; multi-value external returns | `IdleCDO*`, `IdleCreditVault` | S3 | G1, G5, G14, G15 | -| `using SafeERC20Upgradeable for …`: `safeTransfer`, `safeTransferFrom`, `safeIncreaseAllowance` | `IdleCDOCreditVault`, `GuardedLaunchUpgradable`, `IdleCreditVault` | S3 | — | -| `try this.f(x) { … } catch { … }` (three sites, all bare `catch`, all self-calls) | `IdleCDOEpochVariant.startEpoch`, `_stopEpoch`, `getInstantWithdrawFunds` | S3 | G3 | -| `int256` storage (`interestForOverUnderPerformance`), locals, tuple returns `(uint256, int256)`, `int256(x)`/`uint256(y)`, unary `-`, signed compare, signed checked arithmetic | `_virtualPriceAux`, `_updateAccounting`, `_calcInterestWithdrawRequest`, `IdleCreditVault` | S4 | G9 | -| `pragma solidity 0.8.10;`, `^0.8.0` / `^0.8.2` in OZ files, `import` directives, remappings, multi-file source units | all | S5 | — | -| `initialize` (`new IdleCDOTranche`, `string` concatenation, `abi.encodePacked`, `10**decimals()`), constructors, function-typed locals (`_getAARatio`), `tx.origin` | `IdleCDOCreditVault`, `IdleCreditVault` | out of scope: excluded entry points (S6) | — | - -Two facts shape the design more than any other: - -- **solc already flattens.** The `storageLayout` output for a most-derived - contract lists every inherited variable with its final slot and offset, and - `linearizedBaseContracts` gives the C3 order. The importer never computes - an inheritance layout itself; this is why gap G7 (absolute slots that - cannot serve two chains) does not exist on the import route. -- **The importer owns its AST.** Gaps G8–G10, G12, G13 and G16 are - limitations of the `verity_contract` surface syntax. The imported term is - built in `MetaM` from solc declaration ids; Lean identifiers, keyword - clashes, and definition order are not constraints. Gaps that concern the - *executable core* (G1, G2, G4, G14) do apply and are addressed in S2/S3. - ---- - -## 4. Design decisions that span slices - -### 4.1 Program shape: callee-first function table - -`Syntax.lean` gains a function context in the same de Bruijn style as `Ctx` -and `Layout`: - -- `Sig := (params : Ctx) × (rets : List Ty)`; `Fns := List Sig`. -- `FVar : Fns → Sig → Type` indexes an already-registered function. -- `Expr`/`Stmt` become `Expr L F Γ t` / `Stmt L F Γ r`; `Expr.call : - FVar F σ → Args L F Γ σ.params → …`. - -Each Solidity function (internal or external, from any contract in the -linearization) is registered as its own Lean constant in callee-first order. -`Fn.meaning body slots fns env` takes an `FnEnv F` term built from the -already-registered constants (the same pattern as `slotsTerm`). Consequences: - -- Recursion (direct or mutual) is unrepresentable and is rejected by the - parser with a diagnostic naming the cycle. The Pareto sources are expected - to be recursion-free; S1's acceptance run on the vendored sources confirms - it (a hit would be a scoped rejection of that function, not a design - change). -- `#print Child.bump` shows the call to `Base._bump` by constant name, so the - "parsed AST under its meaning" readability property of the POC is kept. -- Kernel terms stay per-function sized; nothing grows with the size of the - whole contract except `step`. - -### 4.2 Storage: solc layout is the only layout - -Every entry in solc's `storageLayout.storage` is either an **imported field** -(type in the accepted set, gets a `Slot` handle, a `Storage` member and, -if public, a getter) or an **opaque field** (any other type: `__gap` arrays, -`bytes32`, `string`, `address[]`, interface-typed fields). Opaque fields keep -their slot reserved, appear in an `opaqueFields` audit constant, and are not -in `Storage`; any body that reads or writes one is rejected at that -statement. This replaces the POC's "unaccounted layout field" rejection and -is the mechanism that lets each slice widen the type set independently. - -Sub-word (packed) fields and struct members are read and written on the -word at their slot with shift/mask (S2). Nested mappings and struct members -inside mappings derive a **logical** inner base with a provably injective -`Nat` pairing offset by `2^256` so it can never equal a solc slot (S2). -This is consistent with the existing "logical keys, not keccak" trust -stance and needs no axiom. - -### 4.3 External behaviour is a parameter, never an axiom - -Imported definitions that perform external calls take an explicit -`world : Sol.CallModel` argument (S3). `CallModel` has a `call` field for -state-changing calls and a `staticCall` field that cannot write. Theorems -quantify over `world` and state their assumptions as hypotheses; the S6 -smoke and later work may instantiate `world` with the imported strategy's -own meaning through `Verity.MultiContract` hops. `try this.f()` is -`Contract.selfCall` under `Contract.tryWith` and does not touch `world`. - -### 4.4 Entry points and `step` - -`step` stays the disjunction of every public/external function and public -getter, in a fixed order: the target contract's functions in source order, -then each base in linearization order, then getters in layout order. S6 -adds an explicit entry-point allowlist for partial imports; the excluded -functions are recorded in a registered `unmodeledEntryPoints` constant that -the trust docs and the smoke's spec must cite. - -### 4.5 Registered sources - -`registeredSource : String` becomes a hard-coded manifest -`registeredSources : List (String × String)` (logical path, package-relative -path) in `Importer.lean`, still part of the digest, still checked for -canonical package containment. The lakefile tracks each smoke directory with -`input_dir` plus a `lean_lib` with `needs`, mirroring the `VaultFromSolidity` -target. - ---- - -## 5. Slice S1: inheritance - -**Goal**: import a most-derived contract whose bases live in the same source -unit, with solc's linearization, flattened layout, virtual dispatch, `super`, -abstract bases, internal function calls, and opaque storage fields. - -### Files - -- `Contracts/VaultFromSolidity/Importer/Syntax.lean`: `Sig`, `Fns`, `FVar`, - `Args`; index `Expr`/`Stmt` by `F`; `Expr.call`, `Stmt.callStmt`; - `Ret` generalized to `List Ty` (parser still accepts 0 or 1 until S3). -- `Contracts/VaultFromSolidity/Importer/Semantics.lean`: `FnEnv`, - `Fn.meaning` with `fns`; call meaning in Solidity evaluation order - (arguments left to right, then callee body under the caller's - `msg.sender`/`msg.value`). -- `Contracts/VaultFromSolidity/Importer/Importer.lean`: - - accept several `ContractDefinition`s; `solidity_contract A from "f.sol" - contract "Name"` selects the target (defaults to the sole non-abstract - contract); - - accept `abstract`, `baseContracts`, `InheritanceSpecifier`, - `linearizedBaseContracts` with more than one id, `contractDependencies` - (must reference contracts in the unit), body-less `virtual` functions - whose signature is implemented in the linearization (`fullyImplemented` - of the target must be true), `override`/`overrides` metadata and - `baseFunctions`; - - virtual dispatch and `super` resolved by signature over the target's - `linearizedBaseContracts` (most-derived first for calls; next-after-the - -defining-contract for `super`), cross-checked against the AST's - `referencedDeclaration`/`baseFunctions` and rejected on disagreement; - - storage entries matched by `astId` to the declaring contract in the - linearization instead of `logicalPath:contractName`; opaque fields; - - callee-first registration order with cycle rejection; `_` - -prefixed Lean names for base functions to avoid collisions; - - `step` order per Section 4.4; - - `registeredSources` manifest. -- `Contracts/SolidityImportSmoke/Inheritance/{Inheritance.sol, - Inheritance.lean, Spec.lean, Proofs.lean}`: synthetic `Storage`-like base, - `Pausable`-like base with `internal virtual _pause`, `Ownable`-like base, - child overriding `_pause` and calling `super._pause()`, one diamond - (`Base` reached twice). Only POC types plus `address` scalars. -- `Contracts/SolidityImportSmoke/Inheritance/scripts/inheritance_test.py` - (same disposable-package pattern as `solidity_importer_test.py`). -- `lakefile.lean`, `scripts/check_contract_structure.py`, docs per rule 6. - -### Tests - -- Acceptance suite runs the full POC suite unchanged, then: - - builds the synthetic smoke; probes slot numbers of inherited fields - against solc's layout (`[base slots…, child slots…]`); - - `#print Child.go` shows `Expr.call` to `Child__pause` and - `#print Child__pause` shows the `super` hop to `PausableLike__pause`; - - mutation: swapping the `is` order changes the linearization and the slot - probe; changing the override body breaks the dispatch theorem; adding a - public function to a base adds a `step` disjunct and breaks the - invariant theorem; importer/syntax/semantics content edits move the - digest; - - rejections with positions: direct and mutual recursion, unimplemented - virtual in a non-abstract target, a body touching an opaque field, - `is` referencing a contract from another file (until S5), function - overloading by parameter type (same name, different signature: rejected - in v1 to keep name resolution simple). -- Proofs (no `sorry`): `dispatch_is_child` (an entry point in the base that - calls the virtual observes the child's override), `super_runs_parent`, - `paused_invariant : PreservedBy … step`, and one `*_meets_spec` per entry - point. - -### Acceptance - -- `lake build VaultFromSolidity SolidityImportSmokeInheritance` green; both - python suites pass; axiom audit reports only `propext`, `Quot.sound` for - every theorem in both proof files. -- Dry run on the vendored Pareto sources is **not** required here (they - need S2–S5), but the slice records in its PR the list of Pareto functions - that are recursion-free (expected: all). - -### Non-goals - -Modifiers, events, constructors, initializers, multi-file units, packed or -non-word storage, external calls, `this.`, overloading, function-typed -values, libraries, `using for`. - ---- - -## 6. Slice S2: modifiers, structs, and the declaration surface - -**Goal**: the declaration and statement surface that OpenZeppelin parents -and the Pareto bodies use, without external calls or signed integers. May -land as three PRs in this order: S2a modifiers and events, S2b storage -shapes, S2c statements and expressions. One roadmap slice, one acceptance -suite. - -### Design - -- **Modifiers** are inlined at parse time; `Semantics.lean` never sees `_`. - A function with modifiers `m1 m2` and body `B` is parsed as - `m1-pre; (m2-pre; let r ← block B; m2-post); m1-post; return r`. A - `block` is a new `Stmt` form whose `return` exits the block only, so - `nonReentrant`'s postlude runs after an early `return` exactly as in - Solidity, and a `revert` anywhere still aborts the whole call. Modifier - bodies may call internal functions (S1 machinery); a modifier with more - than one `_`, or with arguments (`reinitializer(uint8)`), is rejected. -- **Events** parse to `Stmt.emit name args` whose meaning is - `Verity.emitEvent`; events are part of the state and available to specs - but no slice proves anything about them. -- **Storage shapes**: `bool`, `uint8`, `address`, `int256` (tag only; S4 - gives it arithmetic) scalars at any `offset` with `numberOfBytes` 1/20/32, - read as shift-and-mask on the word and written by masked merge; - `mapping(uint256 => uint256)`, `mapping(address => mapping(uint256 => - uint256))`, `mapping(address => Struct)` with word-sized members, storage - pointer locals `S storage p = m[k]`, member read/write, `delete m[k]` - (writes every member to zero). Logical inner bases per Section 4.2. - `Storage` exposes `apr0Users : Address → Apr0UserData` as a Lean structure. -- **Constants** (`constant` state variables with literal or - literal-arithmetic initializers, `365 days`, `10**18`, `type(uint256).max`) - are evaluated at import time to `Nat` literals; the evaluator is part of - the digest and rejects non-literal initializers. -- **Statements/expressions**: `if/else`, nested blocks, `bool` locals, - ternary, `&&`/`||` with short circuit (both operands are pure in the - accepted subset, so evaluation order is not observable; the parser rejects - operands with calls until S3 decides), `!`, `==`/`!=`/`<`/`<=`/`>`/`>=` on - `uint256`, `address`, `bool`; checked `*`, `/`, `%` (`Panic(0x11)`, - `Panic(0x12)`) via `Verity.Stdlib.Math`; `require(cond, "…")` with the - string kept verbatim; `revert E(args)` with word-sized arguments; - `block.timestamp`, `address(this)`; assignment to locals and named return - variables; `return;` in unit functions. - -### Files - -- `Syntax.lean`, `Semantics.lean`, `Importer.lean` as above; new allowed - node kinds: `ModifierDefinition`, `ModifierInvocation`, - `PlaceholderStatement`, `EventDefinition`, `EmitStatement`, - `StructDefinition`, `UserDefinedTypeName`, `UnaryOperation`, - `Conditional`, `TupleExpression` (parenthesized only), `MemberAccess` on - structs, `ElementaryTypeNameExpression` (for `type(uint256).max`), `Literal` - with `subdenomination`. -- `Verity/Core.lean` or `Verity/Core/Semantics.lean`: word-level packed - read/write helpers and their `solidity_simp` lemmas if not already - exported (`compiledPackedRead` exists on the compiler side; the executable - core needs the same two functions with proofs `packed_read_write`, - `packed_write_other_field`). No `ContractState` field is added. -- `Verity/Proofs/Stdlib/SolidityImport.lean`: `solidity_simp` extended with - the new primitives and the pairing lemmas. -- `Contracts/SolidityImportSmoke/Modifiers/…` (OZ-shaped `Ownable`, - `Pausable`, `ReentrancyGuard`, `Initializable` re-typed in one file with - `pragma ^0.8.33`, plus a child using `nonReentrant` with an early return), - `Contracts/SolidityImportSmoke/Storage/…` (packed bools at the same slot, - a struct mapping, a nested mapping, `delete`), each with `Spec.lean`, - `Proofs.lean`, and a python acceptance script. - -### Tests - -- Modifier order mutation (swap `nonReentrant whenNotPaused`) breaks a - theorem that pins the revert reason order; dropping a postlude breaks - `status_restored`; a `return` before `_` in the synthetic body still - restores `_status` (theorem, not just a test). -- Packed layout: solc's `offset`/`numberOfBytes` drive the mask; mutation - reordering two bools in one slot changes the probe and keeps proofs - passing (names bind to solc offsets, as slots do today). -- Struct/nested mapping: `set_then_get` and frame theorems - (`other_key_unchanged`, `other_member_unchanged`) proved from the pairing - injectivity lemma; `#print axioms` on that lemma shows none. -- Rejections: modifier with two `_`, modifier arguments, struct with a - dynamic member, `mapping` with struct key, `unchecked`, loops, - inline assembly, `string` locals, arrays, `delete` on a scalar of opaque - type, `require` without a message is *accepted* (empty reason). - -### Acceptance - -- All prior suites green; new smokes build; axiom audit clean. -- Dry run: the importer applied to the vendored OZ parents alone (with the - OZ pragmas rewritten to `^0.8.33` in the smoke copy) accepts - `OwnableUpgradeable`, `PausableUpgradeable`, `ReentrancyGuardUpgradeable`, - `Initializable` bodies except those that call `AddressUpgradeable` - (`initializer`'s `isContract` check, which lands in S3 as a static call - primitive) and `_msgData` (`bytes calldata`, out of scope; never called by - the Pareto sources). - -### Non-goals - -External calls, libraries, `try`, signed arithmetic, dynamic arrays, -strings and `bytes`, `enum`, parameterized modifiers, `unchecked`, loops, -events in specs, gas. - ---- - -## 7. Slice S3: external calls and try/catch - -**Goal**: typed interface calls, the three OpenZeppelin SafeERC20 helpers -the Pareto sources use, multi-value returns, and `try this.f(args) {…} -catch {…}`, with callee behaviour as an explicit model parameter. - -### Design - -- `Sol.CallModel` in `Semantics.lean`: - `call : Address → Selector → List Uint256 → Contract (List Uint256)` and - `staticCall : Address → Selector → List Uint256 → ContractState → - Except String (List Uint256)`. Selectors are computed from the interface - signature with the existing kernel-computable keccak engine (already used - for `functionSelector` checks elsewhere in the repo), so a `CallModel` - instantiation can be keyed by real ABI selectors later. -- Interface calls: `I(expr).f(args)` where `I` is an `interface` (or a - contract used as an interface, as in `IdleCreditVault(strategy)`) parse to - `Expr.extCall`/`Stmt.extCallStmt` with the target address expression, - selector, word-typed arguments, and word-typed returns (0..n). `view` - interface methods route to `staticCall`; a `view` importing function may - only contain static calls (checked from `stateMutability`). -- Library helpers: `using SafeERC20Upgradeable for …` plus - `token.safeTransfer(to, v)`, `safeTransferFrom(from, to, v)`, - `safeIncreaseAllowance(spender, v)` parse to `Expr.libCall` over a closed - `LibFn` enumeration. Their meaning is written directly in `Semantics.lean` - as the ERC-20 call through `world.call` plus the OZ optional-return check - (`_callOptionalReturn`: revert unless returndata is empty or decodes to - `true`); `safeIncreaseAllowance` is `allowance` static read, checked add, - `approve`. The library bodies themselves (assembly, `abi.encodeWithSelector`) - are never imported; this is a documented trusted translation of three - functions, pinned to the OZ 4.9.6 source hash. -- `try this.f(args) { S } catch { C }`: `f` must be an external function of - the same contract; meaning is `Contract.tryWith (Contract.selfCall (f args - with sender := address(this), value := 0)) (fun _ => S) C`. Only bare - `catch` is accepted (all three Pareto sites are bare). Returndata is not - bound. -- Multi-value returns: `(uint256 a, uint256 b) = e;` and - `(a, b) = e;` for internal and external calls; named return variables of - any arity. -- `address(this)` compares and `msg.sender != address(this)` guards are S2 - expressions; `IERC20Detailed(_token).balanceOf(address(this))` is the - canonical static-call test. - -### Files - -- `Syntax.lean`, `Semantics.lean` (`CallModel`, `extCall`, `libCall`, - `tryCatch`, tuple binding), `Importer.lean` (new nodes: - `TryStatement`, `TryCatchClause`, `FunctionCall` with `tryCall = true`, - `UsingForDirective`, `InterfaceDefinition` / `contractKind = interface`, - `FunctionCall` of kind `typeConversion` for `I(addr)` and `address(x)`, - `MemberAccess` on interface-typed expressions, `Identifier` `this`). -- `Verity/Core.lean`: lemmas `tryWith_success`, `tryWith_revert_rollback`, - `selfCall_sender` if missing; added to `solidity_simp`. -- `Contracts/SolidityImportSmoke/Calls/…`: an interface, a token param, - `safeTransfer`, a static read, a self-call `try` with a success path and a - revert path (`sendFunds` reverts when `amount > balance`), a two-value - external return. - -### Tests - -- Theorems: `catch_iff_selfcall_reverts`, `catch_state_is_snapshot` - (post-state of the catch branch equals the pre-`try` state plus the - handler's writes), `success_path_writes`, `static_call_does_not_write`, - `safeTransfer_reverts_on_false` (for any `world` returning `false`), - all universally quantified over `world`. -- Mutations: remove the handler write; make `sendFunds` revert - unconditionally; swap argument order in an interface call (selector and - argument list change, theorem breaks); change the OZ helper hash pin - (import fails). -- Rejections: `try` on a non-self target, `catch Error(string)`, - `catch Panic`, `.call{value: …}`, `delegatecall`, `payable`, library - functions outside the closed set, interface methods with dynamic - arguments or returns (`bytes`, `string`, arrays), `new`. - -### Acceptance - -- All suites green; axiom audit clean (`CallModel` is a binder, so - `#print axioms` is unchanged by design). -- Dry run on the vendored `GuardedLaunchUpgradable` and `IdleCDOStorage` - (pragmas rewritten) accepts every function except `_deployTranche`-style - code, and the S2 leftover `initializer` now accepts `isContract` as a - static primitive (`extcodesize` modelled as a `staticCall` on a reserved - selector in `CallModel`). - -### Non-goals - -Modeled callees inside the importer (instantiating `world` with another -imported contract is proof-side work after S6), ETH value, reentrancy -adversary registries, returndata in `catch`, dynamic ABI, events in specs. - ---- - -## 8. Slice S4: signed integers - -**Goal**: `int256` as a first-class accepted type with Solidity 0.8 checked -semantics, enough for `_virtualPriceAux`, `_updateAccounting` and -`_calcInterestWithdrawRequest`. - -### Design - -- `Ty.int` denotes `Verity.Core.Int256`. Storage scalars (`FieldType.int256` - already exists in the layout reports), locals, parameters, single and - tuple returns. -- Literals: non-negative literals typed `int256` by solc; unary minus on - any `int256` expression is checked negation (`Panic(0x11)` on `minValue`). -- Conversions `int256(u)` / `uint256(i)` are bit reinterpretation - (`Int256.ofUint256` / `toUint256`, no range check) exactly as Solidity - 0.8 defines explicit conversion between same-width signed/unsigned types. -- Checked `+ - * /` and `%` via `Int256.safeAdd/safeSub/safeMul/safeDiv/ - safeMod` (`Panic(0x11)` overflow, `Panic(0x12)` division by zero; `minValue - / -1` is `Panic(0x11)`); `-=`/`+=`/`*=`/`/=` on `int256` lvalues; - comparisons via `slt/sgt/sle/sge`; ternary on `int256`. -- `solidity_simp` gains the `Verity/Proofs/Stdlib/Int256.lean` bridge - lemmas (wrapping equals unbounded `Int` on success). PR #2414 removes - `native_decide` from the sign-bit lemmas; S4 depends on it being merged - and its acceptance suite re-checks that every lemma it uses reports no - `Lean.ofReduceBool`. - -### Files - -- `Syntax.lean`, `Semantics.lean`, `Importer.lean` (type strings - `int256`, `t_int256`, `t_rational_*` literal typing, `UnaryOperation` `-`, - `FunctionCall` kind `typeConversion` between `int256`/`uint256`). -- `Verity/Proofs/Stdlib/SolidityImport.lean`: signed lemma set. -- `Contracts/SolidityImportSmoke/Signed/…`: a re-typed `_virtualPriceAux` - (`gain = int(nav) - int(lastNav)`, fee split, `max(gain, -int(lastNavBB))` - waterfall, `uint256(int256(x) + gain)`), with `pragma ^0.8.33`. - -### Tests - -- Theorems: `bb_absorbs_loss_first`, `aa_never_below_zero_when_bb_covers`, - `conversion_roundtrip`, overflow premises stated as `Int` inequalities; - `gain_split_sum` (AA gain + BB gain = total gain after fee, as `Int`). -- Mutations: flip a `>` to `>=` in the waterfall; drop the fee term; replace - checked `-` by unchecked wrap (via `unchecked` would be rejected, so the - mutation edits the *spec* instead, as the POC does for Spec.lean). -- Rejections: `int128`/narrow signed types, `>>`/`<<` on `int256`, `int256` - mapping keys, implicit conversions solc would reject anyway (fixture - proves the diagnostic is ours, not solc's). - -### Acceptance - -- All suites green; axiom audit clean; no `native_decide` in any lemma - reachable from the smoke (checked by the suite through `#print axioms` - rejecting `Lean.ofReduceBool`). -- Dry run: the importer accepts `IdleCDOCreditVault._virtualPriceAux`, - `_updateAccounting` and `IdleCDOEpochVariant._calcInterestWithdrawRequest` - bodies from the vendored sources (pragmas rewritten). - -### Non-goals - -`sar`/shifts, narrow signed widths, signed `**`, `int256` in ABI-dynamic -positions, signed constants beyond literals. - ---- - -## 9. Slice S5: solc 0.8.10 and multi-file source units - -**Goal**: compile the Pareto sources unmodified (`pragma solidity 0.8.10;`) -through a second pinned compiler, with imports and remappings resolved -entirely from a vendored, hash-pinned manifest. - -### Findings that fix the design (verified against the real binaries) - -| | solc 0.8.33 (current pin) | solc 0.8.10 | -|-|---------------------------|-------------| -| Binary | `solc-linux-amd64-v0.8.33+commit.64118f21`, sha256 `1274e5c4…5468` | `solc-linux-amd64-v0.8.10+commit.fc410830`, sha256 `c7effacf28b9d64495f81b75228fbf4266ac0ec87e8f1adc489ddd8a4dd06d89` | -| `--no-import-callback` | supported | **absent** (added in 0.8.22) | -| `evmVersion: cancun` | supported | **rejected** (0.8.10 tops out at `london`) | -| AST fields missing in 0.8.10 on the POC Vault | — | `ContractDefinition.usedEvents`, `ErrorDefinition.errorSelector`, `FunctionCall.nameLocations`, `MemberAccess.memberLocation`, `Mapping.keyName/keyNameLocation/valueName/valueNameLocation` | -| Pragma literals | `["solidity","^","0.8",".33"]` | `["solidity","0.8",".10"]` (exact); OZ files use `^0.8.0` / `^0.8.2` | - -### Design - -- **Compiler table**: `Importer.lean` carries a closed table - `{version output string, sha256, evmVersion, supportsNoImportCallback}` - for 0.8.33 and 0.8.10. The pragma of the *target* file selects the - compiler; every file in the manifest must have a pragma satisfied by that - version (exact `0.8.10` or a caret range containing it). The chosen - compiler's sha256 goes into the digest. -- **Fail-closed imports without `--no-import-callback`**: every source is - supplied inline in standard JSON; `settings.remappings` maps - `@openzeppelin/contracts-upgradeable/` and `@uniswap/v2-periphery/` onto - logical paths in the same JSON; solc runs with `cwd` set to an empty - temporary directory and no `--base-path`/`--allow-paths`; the output - `sources` keys must equal the manifest exactly and `ImportDirective. - absolutePath` values must all be manifest keys. Any file-callback - resolution therefore fails. -- **Version-indexed schema**: `allowedNodeFields`/`requiredNodeFields` - become functions of the compiler entry; the differences above are the - initial delta. The first task of the slice is a committed fixture - (`Importer/fixtures/ast-fields-0.8.10.txt`, generated by a script that - walks the AST of the vendored Pareto sources) diffed against the 0.8.33 - fixture; every difference is either allowed with a meaning or rejected. -- **Multi-file**: `ImportDirective` (plain and `{X} from`), several - `SourceUnit`s, `exportedSymbols` across files, `linearizedBaseContracts` - and `referencedDeclaration` ids resolving across files, storage entries' - `contract` field naming another file. Source positions in diagnostics - carry the logical path of the right file. -- **Vendoring**: `Contracts/ParetoFromSolidity/vendor/idle-tranches/…` at - `54502d1` and `…/vendor/openzeppelin-contracts-upgradeable/…` at - v4.9.6, byte-identical, with `vendor/MANIFEST.json` (upstream URL, - commit/tag, path, sha256 per file) checked by a script and the same - hashes recorded in the pareto closure repo's `audit/source-map.yaml`. - Licenses: idle-tranches files are `UNLICENSED`/Apache-2.0 per header, OZ - is MIT; the vendor directory carries both notices. -- **CI**: `.github/actions/setup-solc` gains a second version triple - (`SOLC_IMPORT_LEGACY_VERSION/URL/SHA256`) installed to - `.lake/solidity-import/solc-0.8.10`; `scripts/check_solc_pin.py` checks - both; `Makefile` target `test-solidity-import` runs every importer suite. - Runner architecture must be confirmed (this workspace reports `x86_64`; - official 0.8.10 binaries exist for `linux-amd64` only). - -### Files - -- `Importer.lean` (compiler table, manifest, remappings, multi-file - resolution, version-indexed schema), `Importer/fixtures/*`, - `Importer/scripts/ast_fields_fixture.py`. -- `Contracts/SolidityImportSmoke/Vault810/{Vault.sol, …}`: the POC Vault - with `pragma solidity 0.8.10;` and the same `Spec.lean` and - `ExecutionProof.lean` (copied, not shared), proving the seven theorems - under the other compiler. -- `Contracts/SolidityImportSmoke/MultiFile/…`: a child importing a base - from a second file through a remapping. -- `.github/actions/setup-solc/action.yml`, `.github/workflows/verify.yml`, - `scripts/check_solc_pin.py`, `Makefile`, `TRUST_ASSUMPTIONS.md` (two pins). - -### Tests - -- Compiler substitution: 0.8.33 binary for a `0.8.10` target → "compiler - version mismatch"; tampered 0.8.10 binary → "compiler checksum mismatch"; - a manifest file missing → solc error surfaced with position; an import - resolving outside the manifest (probe writes a file into the temp cwd) → - "unexpected compiler sources"; a pragma outside the table → rejected. -- Schema: the synthetic AST wrapper probes from the POC re-run for 0.8.10 - (`ast`, `metadata`, `typed`, `missing`, `span`, `layout`). -- Seven Vault theorems pass under 0.8.10; slot probe identical to 0.8.33. - -### Acceptance - -- All suites green under both pins; axiom audit clean. -- The vendored Pareto sources compile through the importer's solc - invocation with no solc errors, and the AST-field fixture for 0.8.10 is - committed and fully classified (every field allowed or rejected, none - "unvalidated"). - -### Non-goals - -Any 0.8.x other than 0.8.10 and 0.8.33; `viaIR`; optimizer settings; -`hardhat`/`foundry` build parity; on-chain bytecode matching. - ---- - -## 10. Slice S6: one Pareto smoke - -**Goal**: import `IdleCDOEpochVariant` (most-derived, full chain, unmodified -vendored sources, solc 0.8.10) as a **partial import** with an explicit -entry-point allowlist, and prove one contract-level property about a real -entry point. - -### Smoke selection - -Entry point: `IdleCDOEpochVariant.getInstantWithdrawFunds()`. It exercises -every slice at once: - -- S1: `_checkOnlyOwnerOrManager` (child) → `owner()` (OwnableUpgradeable, - via GuardedLaunchUpgradable) and `IdleCreditVault(strategy).manager()`; - `_handleBorrowerDefault` → `_pause()` (PausableUpgradeable, `internal - virtual`). -- S2: packed bools `isEpochRunning`, `allowInstantWithdraw`, `defaulted`; - `whenNotPaused` on `_pause`; `emit BorrowerDefault(funds)`; `NotAllowed` - custom error through `_checkNotAllowed`. -- S3: `_pendingInstant()` static call to the strategy, `try - this.getFundsFromBorrower(_instantWithdraws)` with `safeTransferFrom` - inside, `_strategy.collectInstantWithdrawFunds(...)` in the success path. -- S4: not on this path; covered by the additional view entry point - `virtualPrice(address)` (→ `_virtualPriceAux`, static calls to the - strategy and the token) which the allowlist also includes so the signed - path is imported from the real source. -- S5: real pragmas, real OZ files, real remappings. - -Property (spec sketch, stated over `view` and a universally quantified -`world`): - -- if the self-call reverts under `world`, the post-state has `defaulted = - true`, `allowInstantWithdraw` unchanged, the epoch contract paused, and - `world.call` was never invoked with the strategy's - `collectInstantWithdrawFunds` selector (stated as a trace predicate on the - `CallModel` log, which S3's `CallModel` records in `ContractState` - returndata-style journal or as an explicit log field of the model, to be - decided in S3 and reused here); -- if it succeeds, `allowInstantWithdraw = true` and the strategy was called - exactly once with the `_pendingInstant()` value. - -Candidate properties beyond this one are listed in the closure repo's -`audit/PROPERTIES.md`; only the two above are in scope. - -### Partial import mechanism - -`solidity_contract IdleCDOEpochVariant from "vendor/idle-tranches/contracts/ -IdleCDOEpochVariant.sol" contract "IdleCDOEpochVariant" entry_points -[getInstantWithdrawFunds, virtualPrice, getFundsFromBorrower]`: - -- every function reachable from the allowlist (internal calls, modifiers, - `super`, `this.` targets) must be fully accepted; anything else in the - contract is not parsed, is listed in the registered - `unmodeledEntryPoints : List String`, and is omitted from `step`; -- getters stay in `step` (they are cheap and needed for the views); -- the trust docs state that `step` covers the allowlist only, and the - smoke's `Spec.lean` header repeats the list. - -This is what makes `initialize` (`new IdleCDOTranche`, `string` -concatenation, `abi.encodePacked`), `_getAARatio`'s function-typed local, -and `IdleCreditVault`-side code stay out without weakening any rule in -Section 1. The alternative, a full import of every entry point, was rejected -for this roadmap: it requires contract creation, `string`/`bytes` memory, -and function-typed values, none of which serve the property. - -### Files - -- `Contracts/ParetoFromSolidity/vendor/…` (S5), `ParetoFromSolidity.lean`, - `Spec.lean`, `Proofs/InstantWithdrawSmoke.lean`, - `scripts/pareto_smoke_test.py`, `lakefile.lean` target - `ParetoFromSolidity`, `docs/VERIFICATION_STATUS.md` row, `README.md` - table row, `AUDIT.md` evidence entry. - -### Tests - -- The smoke script: builds `ParetoFromSolidity`; axiom audit; `#print` - probes that `getInstantWithdrawFunds` unfolds through `Stmt.tryCatch`, - `Expr.libCall .safeTransferFrom`, and `PausableUpgradeable__pause`; - vendored-hash check against `vendor/MANIFEST.json` and against upstream - `54502d1` (network-free: hashes are committed; a separate manual script - re-derives them from the pinned commit); -- mutations on a disposable copy of the vendored source: remove - `defaulted = true` from `_handleBorrowerDefault` → theorem breaks; move - `allowInstantWithdraw = true` into the `catch` → theorem breaks; add - an entry point to the allowlist that touches `initialize` → import - rejected with the position of the first unsupported node; -- unmodeled-entry-point manifest equals the expected list (any new - accepted function must be a deliberate change). - -### Acceptance - -- `lake build ParetoFromSolidity` green on `dgx-spark`; wall-clock and peak - memory recorded in the PR against the baseline in - [`docs/DGX_SPARK_BOTTLENECK_REPORT.md`](DGX_SPARK_BOTTLENECK_REPORT.md); - the kernel-check time of the largest imported function is reported. -- Axiom audit clean; every theorem under 30 lines or allowlisted. -- `TRUST_ASSUMPTIONS.md` has a "Proof-only Pareto import" section listing - the allowlist, `unmodeledEntryPoints`, the three trusted SafeERC20 - translations, both compiler pins, and the `CallModel` parameterization. - -### Non-goals - -`IdleCreditVault` as a modeled callee (instantiating `world`), any -`initialize`/constructor path, `IdleCDOCreditVault` deposits, the full -`step` relation, cross-contract invariants (for example strategy/vault -accounting equalities from the closure repo), events in specs, gas, bytecode, -proxy upgrade safety, `keyring` policy semantics. - ---- - -## 11. Sequencing and dependencies - -``` -S1 inheritance ──► S2 modifiers/structs ──► S3 external calls/try ──► S4 signed ──► S5 solc 0.8.10 ──► S6 Pareto smoke - ▲ - PR #2414 (Int256 lemmas without native_decide) -``` - -- S2 may be split into S2a/S2b/S2c PRs; each keeps the S1 smoke green. -- S3 and S4 touch disjoint constructors and could be developed in parallel - branches, but merge in order (S3 first) to keep tuple-return handling in - one place. -- S5 can be prototyped early (the compiler-table and fixture work does not - depend on S1–S4) but merges after S4 so the fixture classification covers - the final node set. -- Relative size: S1 large, S2 large (three PRs), S3 medium, S4 small, - S5 medium, S6 medium. - ---- - -## 12. Risks and open questions - -| Risk | Impact | Mitigation / decision point | -|------|--------|-----------------------------| -| `super` annotation in solc's AST is relative to the defining contract, not the imported most-derived contract | wrong dispatch would be a silent semantic bug | S1 resolves `super` itself over the target's linearization and treats disagreement with the AST as a rejection; a synthetic diamond test pins it | -| Kernel-check time of large intrinsically typed bodies (`_stopEpoch` is ~190 lines) | S6 build time on `dgx-spark` | per-function constants (4.1); S6 records timings; if a body exceeds budget, split it via the allowlist rather than weakening checks | -| Packed-field semantics duplicated between compiler (`compiledPackedRead`) and the executable core | drift | S2 proves the two agree on word inputs (`width < 256` calculation, already the compiler's argument) | -| `CallModel` call log representation | S6 property needs "the strategy was called once" | decide in S3: explicit journal on the model side, never a `ContractState` field (EVMYulLean exhaustive matches) | -| Function-typed local in `_getAARatio` | blocks `getApr`/`getCurrentAARatio` | out of allowlist; if ever needed, a narrow rewrite (`cond ? f : g` used only in call position → `if` at each call site) is a separate slice | -| solc 0.8.10 has no official `linux-arm64` binary | CI on non-x86 runners | confirm runner architecture before S5; fallback is building solc from source with a pinned hash, documented as a trust surface | -| OZ `initializer` modifier uses `AddressUpgradeable.isContract` (`extcodesize`) | needed only if `initialize` were modeled | out of allowlist in S6; S3 models `isContract` as a reserved static primitive so the OZ file still parses | -| `require` reason strings and panic codes are model strings, not ABI revert bytes | unchanged from the POC | keep the existing trust wording; no slice claims revert-data equivalence | -| Upstream `54502d1` sources contain `UNLICENSED` headers | vendoring | vendor with the upstream notice, proof-only use, confirm with the closure repo owners before S5 merges | - ---- - -## 13. Gap coverage summary - -| Gap | Route on this roadmap | -|-----|-----------------------| -| G1 modeled callee | `CallModel` parameter (S3); instantiation with the imported strategy is post-S6 | -| G2 nested mappings / struct mappings are stubs | real logical keys via injective pairing (S2) | -| G3 `selfCall` with arguments | `try this.f(args)` (S3) | -| G4 packed `bool`/`address` fields | shift/mask on solc offsets (S2) | -| G5 interface-typed storage fields | opaque field (S1) + typed call on `I(addr)` expressions (S3) | -| G6 override cannot see child helpers | dispatch by linearization, no positional elaboration (S1) | -| G7 absolute slots, diamonds rejected | solc layout per chain, C3 accepted (S1) | -| G8–G10 statement/expression forms | importer-owned AST (S2) | -| G11 `view` frame theorem | not generated by the importer; `view` functions are ordinary definitions, static-call-only (S3) | -| G12, G13, G16 naming and ordering | non-issues: declaration ids, callee-first registration (S1) | -| G14 multi-value interface returns | tuple returns (S3) | -| G15 cyclic `linked_contracts` | `CallModel` needs no binding order (S3) | -| G17 gaps, `bytes32`, `string`, `uint8` storage | opaque fields (S1); `uint8` packed scalar (S2) | -| G18 modifiers calling helpers, postludes, abstract functions | inlined modifiers with blocks (S2), abstract bases (S1) | diff --git a/docs/VERIFICATION_STATUS.md b/docs/VERIFICATION_STATUS.md index 7087556a80..2007cdd7b7 100644 --- a/docs/VERIFICATION_STATUS.md +++ b/docs/VERIFICATION_STATUS.md @@ -39,14 +39,12 @@ EVM Bytecode | ERC20 | 22 | Baseline | `Contracts/ERC20/Proofs/` | | ERC721 | 11 | Baseline | `Contracts/ERC721/Proofs/` | | Vault | 9 | Baseline | `Contracts/Vault/Proofs/` | -| VaultFromSolidity | 7 | Proof-only import | `Contracts/VaultFromSolidity/Proofs/` | -| SolidityImportSmoke | 20 | Proof-only import | `Contracts/SolidityImportSmoke/` | | ReentrancyExample | 5 | Complete | `Contracts/ReentrancyExample/Contract.lean` | | ReentrancyRelyGuarantee | 10 | Semantic | `Contracts/ReentrancyRelyGuarantee/Contract.lean` | | CryptoHash | 0 | No specs | `Contracts/CryptoHash/Contract.lean` | -| **Total** | **356** | **✅ 100%** | — | +| **Total** | **329** | **✅ 100%** | — | -> **Note**: Stdlib (0 internal proof-automation properties) is excluded from the contract-spec theorem table above but included in overall coverage statistics (356 total properties). +> **Note**: Stdlib (0 internal proof-automation properties) is excluded from the contract-spec theorem table above but included in overall coverage statistics (329 total properties). Layer 1 uses macro-generated EDSL-to-`CompilationModel` bridge theorems backed by a generic typed-IR compilation-correctness theorem ([`TypedIRCompilerCorrectness.lean`](../Compiler/TypedIRCompilerCorrectness.lean)). Tuple/bytes/fixed-array/dynamic-array/string parameters now stay inside that proof path when they are carried as ABI head words/offsets. Advanced constructs beyond that typed-IR head-word surface (linked libraries, ECMs, fully custom ABI behavior) are still expressed directly in `CompilationModel` and trusted at that boundary. Higher-order internal helpers (function-pointer parameters, [#1747](https://github.com/lfglabs-dev/verity/issues/1747)) are eliminated by a compile-time monomorphization pre-pass that runs before any lowering, so the `CompilationModel` only ever contains first-order helpers: these calls are covered by the existing first-order proof path and introduce no new boundary trust. @@ -204,8 +202,6 @@ Also note that the macro-generated `*_semantic_preservation` theorems are not co |----------|----------|------------| | ERC20 | 86% (19/22) | 3 proof-only | | Vault | 0% (0/9) | 9 proof-only | -| VaultFromSolidity | 0% (0/7) | 7 proof-only | -| SolidityImportSmoke | 0% (0/20) | 20 proof-only | | ERC721 | 100% (11/11) | 0 | | SafeCounter | 100% (25/25) | 0 | | ReentrancyExample | 100% (5/5) | 0 | @@ -221,11 +217,11 @@ Also note that the macro-generated `*_semantic_preservation` theorems are not co | Counter | 74% (23/31) | 8 proof-only | | Stdlib | 0% (0/0) | 0 proof-only | -**Status**: 72% coverage (255/356), 101 remaining exclusions all proof-only +**Status**: 78% coverage (255/329), 74 remaining exclusions all proof-only -- **Total Properties**: 356 +- **Total Properties**: 329 - **Covered**: 255 -- **Excluded**: 101 (all proof-only) +- **Excluded**: 74 (all proof-only) **Proof-Only Properties (74 exclusions)**: Internal proof machinery that cannot be tested in Foundry. diff --git a/lakefile.lean b/lakefile.lean index 9b25773a76..4108b72854 100644 --- a/lakefile.lean +++ b/lakefile.lean @@ -22,75 +22,6 @@ lean_lib «Verity» where .one `Verity.Proofs.LoopSimulationResultAware ] -input_file vaultSolidity where - path := "Contracts/VaultFromSolidity/Vault.sol" - text := false - -input_file vaultLeanImporter where - path := "Contracts/VaultFromSolidity/Importer/Importer.lean" - text := false - -input_file vaultLeanSyntax where - path := "Contracts/VaultFromSolidity/Importer/Syntax.lean" - text := false - -input_file vaultLeanSemantics where - path := "Contracts/VaultFromSolidity/Importer/Semantics.lean" - text := false - -input_file vaultSolc where - path := ".lake/solidity-import/solc" - text := false - -input_file vaultBuildPolicy where - path := "lakefile.lean" - text := false - -lean_lib «VaultSolidityImporter» where - globs := #[.one `Contracts.VaultFromSolidity.Importer.Syntax, - .one `Contracts.VaultFromSolidity.Importer.Semantics, - .one `Contracts.VaultFromSolidity.Importer.Importer] - -lean_lib «VaultFromSolidity» where - globs := #[.one `Contracts.VaultFromSolidity.VaultFromSolidity, - .one `Contracts.VaultFromSolidity.Spec, - .one `Contracts.VaultFromSolidity.Proofs.ExecutionProof] - needs := #[vaultSolidity, vaultLeanImporter, vaultLeanSyntax, vaultLeanSemantics, vaultSolc, - vaultBuildPolicy] - -input_file inheritanceSolidity where - path := "Contracts/SolidityImportSmoke/Inheritance/Inheritance.sol" - text := false - -lean_lib «SolidityImportSmokeInheritance» where - globs := #[.one `Contracts.SolidityImportSmoke.Inheritance.Inheritance, - .one `Contracts.SolidityImportSmoke.Inheritance.Spec, - .one `Contracts.SolidityImportSmoke.Inheritance.Proofs] - needs := #[inheritanceSolidity, vaultLeanImporter, vaultLeanSyntax, vaultLeanSemantics, vaultSolc, - vaultBuildPolicy] - -input_file modifiersSolidity where - path := "Contracts/SolidityImportSmoke/Modifiers/Modifiers.sol" - text := false - -lean_lib «SolidityImportSmokeModifiers» where - globs := #[.one `Contracts.SolidityImportSmoke.Modifiers.Modifiers, - .one `Contracts.SolidityImportSmoke.Modifiers.Spec, - .one `Contracts.SolidityImportSmoke.Modifiers.Proofs] - needs := #[modifiersSolidity, vaultLeanImporter, vaultLeanSyntax, vaultLeanSemantics, vaultSolc, - vaultBuildPolicy] - -input_file structsSolidity where - path := "Contracts/SolidityImportSmoke/Structs/Structs.sol" - text := false - -lean_lib «SolidityImportSmokeStructs» where - globs := #[.one `Contracts.SolidityImportSmoke.Structs.Structs, - .one `Contracts.SolidityImportSmoke.Structs.Spec, - .one `Contracts.SolidityImportSmoke.Structs.Proofs] - needs := #[structsSolidity, vaultLeanImporter, vaultLeanSyntax, vaultLeanSemantics, vaultSolc, - vaultBuildPolicy] - lean_lib «Contracts» where globs := #[ .one `Contracts, diff --git a/scripts/REFERENCE.md b/scripts/REFERENCE.md index b82142aa83..12c3cb253d 100644 --- a/scripts/REFERENCE.md +++ b/scripts/REFERENCE.md @@ -33,7 +33,7 @@ This document is the long-form reference for script responsibilities. Primary guards: - `property_pipeline.py`: consolidated property manifest/coverage pipeline (P7 Cluster C). Subcommands: `check [--only manifest|coverage|lean-sync]` (single manifest parse for all three checks), `extract` (regenerate `test/property_manifest.json`), `report` (coverage statistics; `--format`, `--fail-below`). The five legacy scripts (`check_property_manifest.py`, `check_property_coverage.py`, `check_property_manifest_sync.py`, `extract_property_manifest.py`, `report_property_coverage.py`) remain as thin shims. -- `lean_lint.py`: consolidated Lean structure/hygiene lint runner (P7 Cluster E). Dispatcher over rule modules with `--only/--skip/--list`; rules: `contract_structure`, `paths`, `compilationmodel_split`, `axioms`, `trust_surface_registry`, `storage_layout`, `lean_hygiene`, `split_compiler_test_artifacts`, `rewrite_proof_metadata`, `proof_length`, `spec_named_storage`. Each rule module (the legacy `check_*.py` file) keeps its logic and stays directly runnable for arg-bearing CI invocations. +- `lean_lint.py`: consolidated Lean structure/hygiene lint runner (P7 Cluster E). Dispatcher over rule modules with `--only/--skip/--list`; rules: `contract_structure`, `paths`, `compilationmodel_split`, `axioms`, `trust_surface_registry`, `storage_layout`, `lean_hygiene`, `split_compiler_test_artifacts`, `rewrite_proof_metadata`, `proof_length`. Each rule module (the legacy `check_*.py` file) keeps its logic and stays directly runnable for arg-bearing CI invocations. - `check_axioms.py`: validate AXIOMS.md registry locations and parse `PrintAxioms.lean` dependency output (lean_lint rule `axioms`). - `check_paths.py`: detect case-insensitive checkout hazards and enforce universal Layer-2 bridge quantification (lean_lint rule `paths`). - `check_property_manifest.py`: shim for `property_pipeline.py check --only manifest`. @@ -50,7 +50,6 @@ Primary guards: - `generate_storage_layout_report.py`: emit the per-contract storage layout JSON artifact (`artifacts/storage_layout_report.json`) and human-readable summary (`artifacts/STORAGE_LAYOUT_SUMMARY.md`) for migration/audit review (#1897). The Lean executable `verity-storage-layout-report` is the JSON source of truth; `--check --no-lean` is the drift gate run by `make check`. - `check_lean_hygiene.py` (lean_lint rule `lean_hygiene`; the sorry/native_decide gate) - `check_proof_length.py` (lean_lint rule `proof_length`) -- `check_spec_named_storage.py` (lean_lint rule `spec_named_storage`): opted-in human-facing specs (starting with `Contracts/VaultFromSolidity/Spec.lean`) must use named storage views: no raw `ContractState` accessor (list read from `Verity/Core.lean`), storage field, direct `ContractState` mention, positional projection, or `knownAddresses`. - `check_macro_health.py` - `check_compiler_boundaries.py` - `test_check_struct_mapping_surface_sync.py`: unit coverage for the struct-mapping doc sync guard. @@ -65,7 +64,6 @@ Primary guards: ## Helpers and generators - `official_solc.py`: committed official solc SHA-256 pins and `list.json` helpers. -- `setup_solc_importer.py`: fetch official `list.json`, verify the published digest, install `.lake/solidity-import/solc`. - `check_solc_pin.py --verify-published-checksums` / `make check-solc-published`: live list.json pin check. - `workflow_jobs.py`: shared workflow parsing and command matching helpers. - `generate_print_axioms.py` diff --git a/scripts/check_contract_structure.py b/scripts/check_contract_structure.py index 2020ff195b..9a063a23a0 100755 --- a/scripts/check_contract_structure.py +++ b/scripts/check_contract_structure.py @@ -21,8 +21,6 @@ "ReentrancyRelyGuarantee", # Proof-only rely-guarantee framework example, inline proofs "Ownable", # Mixin facet: named-slot proofs + footprint, no Foundry/Yul twin "OwnedCounterComposed", # Include-host acceptance example; OwnedCounter keeps Yul/difftest - "VaultFromSolidity", # Proof-only imported model with its own focused structure - "SolidityImportSmoke", # Nested proof-only importer smokes (Inheritance, Modifiers, Structs) } # Contracts excluded from property test check @@ -32,8 +30,6 @@ "ReentrancyRelyGuarantee", # Abstract state-transformer proofs, no compiled contract to property-test "Ownable", # Mixin proofs are reused by hosts; no compiled property harness "OwnedCounterComposed", # Proof-composition host; OwnedCounter remains the Foundry target - "VaultFromSolidity", # Imported-model theorems are covered by the focused mutation suite - "SolidityImportSmoke", # Importer smoke theorems are covered by focused Python suites } # Contracts excluded from differential test check @@ -44,8 +40,6 @@ "ReentrancyRelyGuarantee", # No compiled bytecode (abstract proofs), nothing to differential-test "Ownable", # Mixin facet; no dedicated Yul/Foundry twin "OwnedCounterComposed", # Selectors/layout stay on OwnedCounter until bit-identical - "VaultFromSolidity", # Proof-only importer emits no bytecode for differential testing - "SolidityImportSmoke", # Proof-only importer smoke emits no bytecode } # Expected files for each contract (relative to ROOT) diff --git a/scripts/check_solc_pin.py b/scripts/check_solc_pin.py index 59f731657e..466917bd67 100644 --- a/scripts/check_solc_pin.py +++ b/scripts/check_solc_pin.py @@ -26,7 +26,6 @@ SETUP_SOLC_ACTION = ROOT / ".github" / "actions" / "setup-solc" / "action.yml" FOUNDRY_TOML = ROOT / "foundry.toml" TRUST_ASSUMPTIONS = ROOT / "TRUST_ASSUMPTIONS.md" -MACOS_SOLC_SHA256 = official_solc.OFFICIAL_SOLC_SHA256["macosx-amd64"] SOLC_VERSION_RE = re.compile(r'^\s*SOLC_VERSION:\s*"([^"]+)"\s*$', re.MULTILINE) SOLC_URL_RE = re.compile(r'^\s*SOLC_URL:\s*"([^"]+)"\s*$', re.MULTILINE) @@ -145,41 +144,12 @@ def main(argv: list[str] | None = None) -> int: if re.search(r"\bsudo\b", action_text): errors.append(".github/actions/setup-solc/action.yml: solc install step must not require sudo") - importer = ROOT / "Contracts" / "VaultFromSolidity" / "Importer" / "Importer.lean" if linux_sha256 != official_solc.OFFICIAL_SOLC_SHA256["linux-amd64"]: errors.append( ".github/workflows/verify.yml: SOLC_SHA256 must be the official " "linux-amd64 list.json digest" ) - if importer.exists(): - importer_text = _read(importer) - if official_solc.SOLC_LONG_VERSION not in importer_text: - errors.append( - "Contracts/VaultFromSolidity/Importer/Importer.lean: " - f"must pin solcVersionPin {official_solc.SOLC_LONG_VERSION}" - ) - if linux_sha256 not in importer_text: - errors.append( - "Contracts/VaultFromSolidity/Importer/Importer.lean: " - "must pin verify.yml SOLC_SHA256 for linux-amd64" - ) - if MACOS_SOLC_SHA256 not in importer_text: - errors.append( - "Contracts/VaultFromSolidity/Importer/Importer.lean: " - "must pin the official macosx-amd64 solc SHA-256" - ) - if "officialSolcSha256s" not in importer_text: - errors.append( - "Contracts/VaultFromSolidity/Importer/Importer.lean: " - "must accept official solc builds via officialSolcSha256s" - ) - if "/usr/bin/shasum" not in importer_text: - errors.append( - "Contracts/VaultFromSolidity/Importer/Importer.lean: " - "macOS checksum path must be /usr/bin/shasum" - ) - if args.verify_published_checksums: _verify_published_checksums(errors) diff --git a/scripts/check_spec_named_storage.py b/scripts/check_spec_named_storage.py deleted file mode 100644 index 0595ba4323..0000000000 --- a/scripts/check_spec_named_storage.py +++ /dev/null @@ -1,133 +0,0 @@ -#!/usr/bin/env python3 -"""Named-storage gate for human-facing specs. - -A spec should say `v.totalAssets`, not `s.readSlot 0`: a wrong slot number -silently points the promise at another variable, and the reader has to trust a -comment to know what the number means. Opted-in spec files therefore must not -touch `ContractState` directly at all: no raw accessor (`readSlot`, `readMap`, -`readMapUint`, `readTransient`, ...), no raw storage field, no `ContractState` -mention, no positional projection, and no Verity ghost `knownAddresses` -bookkeeping. Names come from a generated storage view (see -`Contracts/VaultFromSolidity/Importer/Importer.lean`); `Storage` is a -kernel-checked structure whose `view` reads through `Slot` handles, so -the gate rejects the accessor names wherever they appear rather than trying -to spot a numeric slot argument after them. - -The raw accessor list is read from `Verity/Core.lean` (every definition in the -`ContractState` namespace plus the storage-backing fields), so a new accessor -is covered without editing this file. - -The gate is opt-in per file: handwritten contracts still state specs over raw -slots and are not listed yet. - -Usage: - python3 scripts/check_spec_named_storage.py -""" - -from __future__ import annotations - -import re -import sys -from pathlib import Path - -from property_utils import ROOT, scrub_lean_code - -SPEC_FILES = ( - "Contracts/VaultFromSolidity/Spec.lean", - "Contracts/SolidityImportSmoke/Inheritance/Spec.lean", - "Contracts/SolidityImportSmoke/Modifiers/Spec.lean", - "Contracts/SolidityImportSmoke/Structs/Spec.lean", -) - -CORE_LEAN = "Verity/Core.lean" - -# Fields of `ContractState` that back storage or its ghost bookkeeping. -STORAGE_FIELDS = ("storageWords", "storageArray", "knownAddresses") - -# Ways to take a `Storage`/`ContractState` value apart without naming a field. -STRUCTURE_ESCAPES = ("mk", "rec", "recOn", "casesOn", "noConfusion") - -_DEF_RE = re.compile( - r"^(?:@\[[^\]]*\]\s*)?(?:private\s+|protected\s+|noncomputable\s+)*" - r"(?:def|abbrev|theorem|instance)\s+([A-Za-z_][\w'?!]*)", - re.M, -) - - -def raw_accessors(core_text: str) -> frozenset[str]: - """Every name defined inside `namespace ContractState` in `core_text`.""" - names: set[str] = set() - depth = 0 - for line in scrub_lean_code(core_text).splitlines(): - stripped = line.strip() - if stripped == "namespace ContractState": - depth += 1 - continue - if stripped == "end ContractState": - depth = max(depth - 1, 0) - continue - if depth and (match := _DEF_RE.match(line)): - names.add(match.group(1)) - return frozenset(names) - - -RAW_ACCESSORS = raw_accessors((ROOT / CORE_LEAN).read_text(encoding="utf-8")) - - -def _identifier_re(names: tuple[str, ...] | frozenset[str]) -> re.Pattern[str]: - alternatives = "|".join(re.escape(name) for name in sorted(names)) - return re.compile(r"(? list[tuple[int, str]]: - """Return `(line, message)` for every raw storage reference in Lean `text`.""" - violations: list[tuple[int, str]] = [] - for line_no, line in enumerate(scrub_lean_code(text).splitlines(), 1): - for match in ACCESSOR_RE.finditer(line): - violations.append((line_no, f"raw `ContractState` accessor `{match.group(0)}`")) - for match in FIELD_RE.finditer(line): - if match.group(0) == "knownAddresses": - violations.append((line_no, "`knownAddresses` ghost bookkeeping")) - else: - violations.append((line_no, f"raw storage field `{match.group(0)}`")) - if CONTRACT_STATE_RE.search(line): - violations.append((line_no, "`ContractState` named directly")) - for match in STRUCTURE_ESCAPE_RE.finditer(line): - violations.append((line_no, f"structure eliminator `{match.group(0)}`")) - for match in PROJECTION_RE.finditer(line): - violations.append((line_no, f"positional projection `{match.group(0)}`")) - return violations - - -def main() -> int: - errors: list[str] = [] - if not RAW_ACCESSORS: - errors.append(f"{CORE_LEAN}: found no definitions in `namespace ContractState`") - for rel in SPEC_FILES: - path = ROOT / rel - if not path.is_file(): - errors.append(f"{rel}: opted-in spec file is missing") - continue - for line_no, message in find_violations(path.read_text(encoding="utf-8")): - errors.append(f"{rel}:{line_no}: {message}; use the named storage view instead") - if errors: - print("Spec named-storage check failed:", file=sys.stderr) - for error in errors: - print(f" - {error}", file=sys.stderr) - return 1 - print(f"Spec named-storage check passed ({len(SPEC_FILES)} opted-in spec files).") - return 0 - - -if __name__ == "__main__": - sys.exit(main()) diff --git a/scripts/consolidation-inventory.md b/scripts/consolidation-inventory.md index 452f5042a5..233f558ea0 100644 --- a/scripts/consolidation-inventory.md +++ b/scripts/consolidation-inventory.md @@ -48,7 +48,6 @@ drift mode unless noted. | check_struct_mapping_surface_sync.py | struct-mapping docs ↔ `Compiler/CompilationModel/Types.lean` surface | lean-grep + doc-grep | exit | — | | check_solc_pin.py | solc pin across verify.yml / foundry.toml / TRUST_ASSUMPTIONS.md / official list.json hashes | repo + doc-grep; `--verify-published-checksums` fetches list.json | exit | official_solc | | official_solc.py | committed binaries.soliditylang.org SHA-256 pins and list.json parsing | imported | — | — | -| setup_solc_importer.py | install official solc after list.json pin check | network + dest file | exit | official_solc | | check_issue_templates.py | `.github/ISSUE_TEMPLATE/*.yaml` structure + log contamination | repo (yaml) | exit | property_utils | | check_docs_workflow_sync.py | docs.yml push paths ↔ pull_request paths | repo (yaml) | exit | — | | check_macro_health.py | dispatcher → check_macro_property_test_generation.py | sub-script | exit | — (dispatch) | diff --git a/scripts/generate_print_axioms.py b/scripts/generate_print_axioms.py index f97e21420a..07e83a48b5 100644 --- a/scripts/generate_print_axioms.py +++ b/scripts/generate_print_axioms.py @@ -21,9 +21,6 @@ PROOF_DIRS = [ROOT / "Verity" / "Proofs", ROOT / "Compiler" / "Proofs"] TRUST_BOUNDARY_FILES = [ ROOT / "Compiler" / "CompilationModel" / "ReservedScratchNames.lean", - ROOT / "Contracts" / "SolidityImportSmoke" / "Inheritance" / "Proofs.lean", - ROOT / "Contracts" / "SolidityImportSmoke" / "Modifiers" / "Proofs.lean", - ROOT / "Contracts" / "SolidityImportSmoke" / "Structs" / "Proofs.lean", ] def _collect_contract_proof_dirs() -> list[Path]: diff --git a/scripts/lean_lint.py b/scripts/lean_lint.py index ad86c789df..9fd2f3c1b0 100644 --- a/scripts/lean_lint.py +++ b/scripts/lean_lint.py @@ -35,7 +35,6 @@ "split_compiler_test_artifacts": "check_split_compiler_test_artifacts", "rewrite_proof_metadata": "check_rewrite_proof_metadata", "proof_length": "check_proof_length", - "spec_named_storage": "check_spec_named_storage", } diff --git a/scripts/setup_solc_importer.py b/scripts/setup_solc_importer.py deleted file mode 100644 index 8eaed0b5c2..0000000000 --- a/scripts/setup_solc_importer.py +++ /dev/null @@ -1,98 +0,0 @@ -#!/usr/bin/env python3 -"""Install the official pinned solc for the Lean Solidity importer. - -Fetches binaries.soliditylang.org `/list.json`, checks that the -published 0.8.33 SHA-256 still matches the committed pin, then downloads that -build to `.lake/solidity-import/solc`. Lake never performs this fetch. -""" - -from __future__ import annotations - -import argparse -import hashlib -import platform -import sys -import urllib.request -from pathlib import Path - -SCRIPT_DIR = Path(__file__).resolve().parent -if str(SCRIPT_DIR) not in sys.path: - sys.path.insert(0, str(SCRIPT_DIR)) - -import official_solc - -ROOT = Path(__file__).resolve().parents[1] -DEFAULT_DEST = ROOT / ".lake" / "solidity-import" / "solc" - - -def host_platform() -> str: - if sys.platform == "darwin": - return "macosx-amd64" - if sys.platform.startswith("linux") and platform.machine() in ("x86_64", "amd64"): - return "linux-amd64" - raise RuntimeError( - f"no official solc pin for {sys.platform} {platform.machine()}" - ) - - -def file_sha256(path: Path) -> str: - digest = hashlib.sha256() - with path.open("rb") as handle: - for chunk in iter(lambda: handle.read(1024 * 1024), b""): - digest.update(chunk) - return digest.hexdigest() - - -def install(dest: Path, platform_name: str) -> None: - expected = official_solc.OFFICIAL_SOLC_SHA256[platform_name] - published = official_solc.published_build(official_solc.fetch_list(platform_name)) - if published["sha256"] != expected: - raise RuntimeError( - f"{platform_name} list.json SHA-256 {published['sha256']} " - f"does not match committed pin {expected}" - ) - dest.parent.mkdir(parents=True, exist_ok=True) - url = official_solc.binary_url(platform_name, published["path"]) - tmp = dest.with_name(dest.name + ".tmp") - try: - request = urllib.request.Request( - url, - headers={"User-Agent": "verity-official-solc/0.8.33"}, - ) - with urllib.request.urlopen(request, timeout=60) as response, tmp.open("wb") as handle: - handle.write(response.read()) - got = file_sha256(tmp) - if got != expected: - raise RuntimeError(f"downloaded {url} SHA-256 {got} != {expected}") - tmp.replace(dest) - dest.chmod(0o755) - finally: - if tmp.exists(): - tmp.unlink() - print(f"installed official solc {official_solc.SOLC_LONG_VERSION} ({platform_name}) at {dest}") - - -def main() -> int: - parser = argparse.ArgumentParser(description=__doc__) - parser.add_argument( - "--dest", - type=Path, - default=DEFAULT_DEST, - help="destination path (default: .lake/solidity-import/solc)", - ) - parser.add_argument( - "--platform", - choices=sorted(official_solc.OFFICIAL_SOLC_SHA256), - help="override host platform (default: detect)", - ) - args = parser.parse_args() - try: - install(args.dest, args.platform or host_platform()) - except (RuntimeError, ValueError, OSError) as err: - print(f"solc importer setup failed: {err}", file=sys.stderr) - return 1 - return 0 - - -if __name__ == "__main__": - raise SystemExit(main()) diff --git a/scripts/setup_solc_slice.py b/scripts/setup_solc_slice.py index cc6318d381..301d1f2a9c 100644 --- a/scripts/setup_solc_slice.py +++ b/scripts/setup_solc_slice.py @@ -1,8 +1,7 @@ #!/usr/bin/env python3 """Install pinned solc 0.8.34 for the function-slice importer. -The Vault importer keeps solc 0.8.33 at `.lake/solidity-import/solc`. -This script writes `.lake/solidity-import/solc-0.8.34` and does not replace it. +The binary is written to `.lake/solidity-import/solc-0.8.34`. Lake elaboration never downloads the compiler. """ diff --git a/scripts/test_check_spec_named_storage.py b/scripts/test_check_spec_named_storage.py deleted file mode 100644 index 74844b03e7..0000000000 --- a/scripts/test_check_spec_named_storage.py +++ /dev/null @@ -1,124 +0,0 @@ -#!/usr/bin/env python3 -"""Tests for the spec named-storage gate.""" - -from __future__ import annotations - -import contextlib -import io -import tempfile -import unittest -from pathlib import Path - -import check_spec_named_storage - -NAMED_SPEC = '''\ -namespace Contracts.VaultFromSolidity.Spec -/-- Never write `s.readSlot 0` or `ContractState.readMap s 2 a` here. -/ -def solvent (v : Storage) : Prop := v.totalAssets = v.totalSupply -def balanceOf_spec (account : Address) (result : Uint256) (v : Storage) : Prop := - result = v.shareBalances account -def depositFits (amount : Uint256) (v : Storage) : Prop := - v.totalAssets.val + amount.val ≤ Verity.Core.MAX_UINT256 -def label : String := "readMap 2" -def storage2 (v : Storage) := v.totalSupply -def readSlotted (v : Storage) := v.totalSupply -end Contracts.VaultFromSolidity.Spec -''' - -RAW_SPEC = '''\ -namespace Contracts.VaultFromSolidity.Spec -def solvent (s : ContractState) : Prop := s.readSlot 0 = s.readSlot 1 -def balance (s : Storage) (a : Address) := ContractState.readMap s 2 a -def raw (s : Storage) := s.storage (1) -def ghost (s : Storage) := s.knownAddresses -end Contracts.VaultFromSolidity.Spec -''' - -# Each of these addressed storage by number while passing the first version of -# the gate, which only looked for a numeric literal right after a short list of -# accessor names on the same line. -BYPASSES = ( - ("def f (v : Storage) := v.readMapUint 3 k", "raw `ContractState` accessor `readMapUint`"), - ("def f (v : Storage) := v.readTransient 1", "raw `ContractState` accessor `readTransient`"), - ("def f (v : Storage) := v.readSlot\n 0", "raw `ContractState` accessor `readSlot`"), - ("def f (s : Storage) := ContractState.readSlot (s) 2", "`ContractState` named directly"), - ("def f (s : Storage) := s.readSlot <| 0", "raw `ContractState` accessor `readSlot`"), - ("def f (s : Storage) := (s.storage) 0", "raw `ContractState` accessor `storage`"), - ("def f (s : Storage) := s.readSlot totalAssetsSlot.slot", "raw `ContractState` accessor `readSlot`"), - ("def f (s : Storage) := s.storageWords key", "raw storage field `storageWords`"), - ("def f (s : Storage) := s.1 0", "positional projection `.1`"), - ("def f (s : Storage) := Storage.casesOn s fun w _ _ => w", "structure eliminator `Storage.casesOn`"), - ("def f (s : Storage) := Verity.ContractState.readSlot s 0", "`ContractState` named directly"), -) - - -class SpecNamedStorageTests(unittest.TestCase): - def run_gate(self, spec: str) -> tuple[int, str]: - with tempfile.TemporaryDirectory() as tmpdir: - root = Path(tmpdir) - for rel in check_spec_named_storage.SPEC_FILES: - target = root / rel - target.parent.mkdir(parents=True, exist_ok=True) - target.write_text(spec, encoding="utf-8") - old_root = check_spec_named_storage.ROOT - output = io.StringIO() - try: - check_spec_named_storage.ROOT = root - with contextlib.redirect_stdout(output), contextlib.redirect_stderr(output): - status = check_spec_named_storage.main() - finally: - check_spec_named_storage.ROOT = old_root - return status, output.getvalue() - - def test_accessors_come_from_core(self) -> None: - accessors = check_spec_named_storage.RAW_ACCESSORS - for name in ("readSlot", "writeSlot", "readMap", "writeMap", "readMapUint", - "readTransient", "storage", "storageMap", "readArray"): - self.assertIn(name, accessors) - self.assertNotIn("Contract", accessors) - self.assertEqual(check_spec_named_storage.raw_accessors("def readSlot := 0\n"), frozenset()) - - def test_named_spec_passes(self) -> None: - status, output = self.run_gate(NAMED_SPEC) - self.assertEqual(status, 0, output) - - def test_raw_slot_spec_fails(self) -> None: - status, output = self.run_gate(RAW_SPEC) - self.assertEqual(status, 1) - self.assertEqual( - check_spec_named_storage.find_violations(RAW_SPEC), - [ - (2, "raw `ContractState` accessor `readSlot`"), - (2, "raw `ContractState` accessor `readSlot`"), - (2, "`ContractState` named directly"), - (3, "raw `ContractState` accessor `readMap`"), - (3, "`ContractState` named directly"), - (4, "raw `ContractState` accessor `storage`"), - (5, "`knownAddresses` ghost bookkeeping"), - ], - ) - self.assertIn("Spec.lean:2:", output) - - def test_bypasses_are_rejected(self) -> None: - for spec, message in BYPASSES: - with self.subTest(spec=spec): - messages = [m for _, m in check_spec_named_storage.find_violations(spec)] - self.assertIn(message, messages) - self.assertEqual(self.run_gate(spec)[0], 1) - - def test_missing_opted_in_file_fails(self) -> None: - old_root = check_spec_named_storage.ROOT - output = io.StringIO() - with tempfile.TemporaryDirectory() as tmpdir: - try: - check_spec_named_storage.ROOT = Path(tmpdir) - with contextlib.redirect_stderr(output): - status = check_spec_named_storage.main() - finally: - check_spec_named_storage.ROOT = old_root - self.assertEqual(status, 1) - self.assertIn("opted-in spec file is missing", output.getvalue()) - - -if __name__ == "__main__": - unittest.main() diff --git a/scripts/test_lean_lint.py b/scripts/test_lean_lint.py index a39f3eaa09..4f170d47ea 100644 --- a/scripts/test_lean_lint.py +++ b/scripts/test_lean_lint.py @@ -24,7 +24,6 @@ def test_expected_rules_registered(self) -> None: "split_compiler_test_artifacts", "rewrite_proof_metadata", "proof_length", - "spec_named_storage", }, ) diff --git a/scripts/verify_sync_spec.json b/scripts/verify_sync_spec.json index 6557fc9713..22e65614af 100644 --- a/scripts/verify_sync_spec.json +++ b/scripts/verify_sync_spec.json @@ -828,7 +828,6 @@ "python3 scripts/generate_evmyullean_native_lowering_report.py --check", "python3 scripts/generate_print_axioms.py --check", "python3 scripts/lean_lint.py --only proof_length", - "python3 scripts/lean_lint.py --only spec_named_storage", "python3 scripts/check_issue_1060_integrity.py", "python3 -m unittest discover -s scripts -p 'test_*.py' -v" ], diff --git a/scripts/verify_sync_spec_source.py b/scripts/verify_sync_spec_source.py index d61924b73e..6e1a34ae5d 100644 --- a/scripts/verify_sync_spec_source.py +++ b/scripts/verify_sync_spec_source.py @@ -702,7 +702,6 @@ '--check', 'python3 scripts/generate_print_axioms.py --check', 'python3 scripts/lean_lint.py --only proof_length', - 'python3 scripts/lean_lint.py --only spec_named_storage', 'python3 scripts/check_issue_1060_integrity.py', "python3 -m unittest discover -s scripts -p 'test_*.py' -v"], 'expected_checks_other_commands': [], diff --git a/test/Vault.t.sol b/test/Vault.t.sol deleted file mode 100644 index c221dd2761..0000000000 --- a/test/Vault.t.sol +++ /dev/null @@ -1,99 +0,0 @@ -// SPDX-License-Identifier: MIT -pragma solidity ^0.8.33; - -import "forge-std/Test.sol"; -import "../Contracts/VaultFromSolidity/Vault.sol"; - -contract VaultTest is Test { - Vault internal vault; - address internal alice = address(0xA11CE); - address internal bob = address(0xB0B); - - function _shareBalanceSlot(address account) internal pure returns (bytes32) { - return keccak256(abi.encode(account, uint256(2))); - } - - function setUp() public { - vault = new Vault(); - } - - function test_InitialState() public view { - assertEq(vault.totalAssets(), 0); - assertEq(vault.totalSupply(), 0); - assertEq(vault.balanceOf(alice), 0); - } - - function test_DepositMintsSharesOneToOne() public { - vm.prank(alice); - vault.deposit(100); - - assertEq(vault.balanceOf(alice), 100); - assertEq(vault.totalAssets(), 100); - assertEq(vault.totalSupply(), 100); - } - - function test_WithdrawBurnsSharesOneToOne() public { - vm.startPrank(alice); - vault.deposit(100); - vault.withdraw(40); - vm.stopPrank(); - - assertEq(vault.balanceOf(alice), 60); - assertEq(vault.totalAssets(), 60); - assertEq(vault.totalSupply(), 60); - } - - function test_WithdrawRevertsWhenSharesInsufficient() public { - vm.prank(alice); - vault.deposit(10); - - vm.prank(alice); - vm.expectRevert(Vault.InsufficientShares.selector); - vault.withdraw(11); - } - - function test_WithdrawRevertsWhenAssetsInsufficient() public { - vm.store(address(vault), _shareBalanceSlot(alice), bytes32(uint256(10))); - vm.store(address(vault), bytes32(uint256(0)), bytes32(uint256(5))); - vm.store(address(vault), bytes32(uint256(1)), bytes32(uint256(10))); - - vm.prank(alice); - vm.expectRevert(Vault.InsufficientAssets.selector); - vault.withdraw(10); - } - - function test_WithdrawRevertsWhenSupplyInsufficient() public { - vm.store(address(vault), _shareBalanceSlot(alice), bytes32(uint256(10))); - vm.store(address(vault), bytes32(uint256(0)), bytes32(uint256(10))); - vm.store(address(vault), bytes32(uint256(1)), bytes32(uint256(5))); - - vm.prank(alice); - vm.expectRevert(Vault.InsufficientSupply.selector); - vault.withdraw(10); - } - - function test_MultipleDepositorsConserveTotals() public { - vm.prank(alice); - vault.deposit(70); - - vm.prank(bob); - vault.deposit(30); - - assertEq(vault.balanceOf(alice), 70); - assertEq(vault.balanceOf(bob), 30); - assertEq(vault.totalAssets(), 100); - assertEq(vault.totalSupply(), 100); - } - - function testFuzz_DepositWithdrawRoundTrip(uint128 amount) public { - vm.prank(alice); - vault.deposit(amount); - - vm.prank(alice); - vault.withdraw(amount); - - assertEq(vault.balanceOf(alice), 0); - assertEq(vault.totalAssets(), 0); - assertEq(vault.totalSupply(), 0); - } -} diff --git a/test/property_exclusions.json b/test/property_exclusions.json index 841b1ccc7f..b02ea2d277 100644 --- a/test/property_exclusions.json +++ b/test/property_exclusions.json @@ -15,37 +15,6 @@ "vaultMinimal_runtime_lowers_native", "vaultMinimal_totalAssets_nativeResultsMatchOn_revert_of_nonzero_value" ], - "VaultFromSolidity": [ - "balance_meets_spec", - "balance_success_spec", - "deposit_meets_spec", - "deposit_success_spec", - "solvent_invariant", - "withdraw_meets_spec", - "withdraw_success_spec" - ], - "SolidityImportSmoke": [ - "bump_meets_spec", - "bump_success_spec", - "dispatch_is_child", - "early_meets_spec", - "get_meets_spec", - "go_meets_spec", - "go_success_spec", - "guarded_reverts_not_owner_before_pause", - "guarded_success_spec", - "makeRev_encodes", - "make_encodes", - "other_member_unchanged_thm", - "pause_meets_spec", - "pause_success_spec", - "paused_invariant", - "set_then_get", - "snapshot_restores_status", - "status_restored", - "super_runs_parent", - "tagged_uses_base_helper" - ], "Counter": [ "getStorage_reads_count", "previewAddTwice_correct", diff --git a/test/property_manifest.json b/test/property_manifest.json index 8aa30a5928..b43566798a 100644 --- a/test/property_manifest.json +++ b/test/property_manifest.json @@ -347,28 +347,6 @@ "transfer_then_balanceOf_recipient_correct", "transfer_then_balanceOf_sender_correct" ], - "SolidityImportSmoke": [ - "bump_meets_spec", - "bump_success_spec", - "dispatch_is_child", - "early_meets_spec", - "get_meets_spec", - "go_meets_spec", - "go_success_spec", - "guarded_reverts_not_owner_before_pause", - "guarded_success_spec", - "makeRev_encodes", - "make_encodes", - "other_member_unchanged_thm", - "pause_meets_spec", - "pause_success_spec", - "paused_invariant", - "set_then_get", - "snapshot_restores_status", - "status_restored", - "super_runs_parent", - "tagged_uses_base_helper" - ], "Vault": [ "balanceOf_meets_spec", "balanceOf_preserves_state", @@ -379,14 +357,5 @@ "vaultMinimal_functions_bridged", "vaultMinimal_runtime_lowers_native", "vaultMinimal_totalAssets_nativeResultsMatchOn_revert_of_nonzero_value" - ], - "VaultFromSolidity": [ - "balance_meets_spec", - "balance_success_spec", - "deposit_meets_spec", - "deposit_success_spec", - "solvent_invariant", - "withdraw_meets_spec", - "withdraw_success_spec" ] } From 43db662bd3d3e8f1b4a010d5003813f64496101a Mon Sep 17 00:00:00 2001 From: Thomas Marchand Date: Thu, 24 Sep 2026 15:20:46 +0200 Subject: [PATCH 11/22] Pin the imported smoke model with a golden Repr snapshot Importer refactors must reproduce the imported CompilationModel exactly; solidity_slice_mutations.py now compares it with model.golden and only rewrites the snapshot with --update-golden. --- Contracts/SoliditySliceSmoke/model.golden | 497 ++++++++++++++++++++++ scripts/solidity_slice_mutations.py | 12 +- 2 files changed, 508 insertions(+), 1 deletion(-) create mode 100644 Contracts/SoliditySliceSmoke/model.golden diff --git a/Contracts/SoliditySliceSmoke/model.golden b/Contracts/SoliditySliceSmoke/model.golden new file mode 100644 index 0000000000..3073a1131d --- /dev/null +++ b/Contracts/SoliditySliceSmoke/model.golden @@ -0,0 +1,497 @@ +{ name := "C_f_slice", + contractId := 1, + fields := [{ name := "position", + ty := Compiler.CompilationModel.FieldType.mappingStruct2 + (Compiler.CompilationModel.MappingKeyType.bytes32) + (Compiler.CompilationModel.MappingKeyType.address) + [{ name := "credit", + ty := Compiler.CompilationModel.StructMemberType.uint256, + wordOffset := 0, + packed := some { offset := 0, width := 128 } }, + { name := "pendingFee", + ty := Compiler.CompilationModel.StructMemberType.uint256, + wordOffset := 0, + packed := some { offset := 128, width := 128 } }, + { name := "lastLossFactor", + ty := Compiler.CompilationModel.StructMemberType.uint256, + wordOffset := 1, + packed := some { offset := 0, width := 128 } }, + { name := "lastAccrual", + ty := Compiler.CompilationModel.StructMemberType.uint256, + wordOffset := 1, + packed := some { offset := 128, width := 128 } }, + { name := "debt", + ty := Compiler.CompilationModel.StructMemberType.uint256, + wordOffset := 2, + packed := some { offset := 0, width := 128 } }, + { name := "collateralBitmap", + ty := Compiler.CompilationModel.StructMemberType.uint256, + wordOffset := 2, + packed := some { offset := 128, width := 128 } }], + isTransient := false, + slot := some 0, + packedBits := none, + aliasSlots := [] }, + { name := "marketState", + ty := Compiler.CompilationModel.FieldType.mappingStruct + (Compiler.CompilationModel.MappingKeyType.bytes32) + [{ name := "totalUnits", + ty := Compiler.CompilationModel.StructMemberType.uint256, + wordOffset := 0, + packed := some { offset := 0, width := 128 } }, + { name := "lossFactor", + ty := Compiler.CompilationModel.StructMemberType.uint256, + wordOffset := 0, + packed := some { offset := 128, width := 128 } }], + isTransient := false, + slot := some 1, + packedBits := none, + aliasSlots := [] }], + immutables := [], + roles := [], + reservedSlotRanges := [], + slotAliasRanges := [], + constructor := none, + functions := [{ name := "f", + params := [{ name := "m_maturity", ty := Compiler.CompilationModel.ParamType.uint256 }, + { name := "id", ty := Compiler.CompilationModel.ParamType.bytes32 }, + { name := "user", ty := Compiler.CompilationModel.ParamType.address }], + returnType := none, + returns := [Compiler.CompilationModel.ParamType.uintN 128, + Compiler.CompilationModel.ParamType.uintN 128, + Compiler.CompilationModel.ParamType.uintN 128], + isPayable := false, + isView := true, + isPure := false, + body := [Compiler.CompilationModel.Stmt.letVar + "_verity_slice_tmp_0" + (Compiler.CompilationModel.Expr.structMember2 + "position" + (Compiler.CompilationModel.Expr.param "id") + (Compiler.CompilationModel.Expr.param "user") + "credit"), + Compiler.CompilationModel.Stmt.letVar + "_verity_slice_tmp_1" + (Compiler.CompilationModel.Expr.structMember2 + "position" + (Compiler.CompilationModel.Expr.param "id") + (Compiler.CompilationModel.Expr.param "user") + "lastLossFactor"), + Compiler.CompilationModel.Stmt.letVar + "_verity_slice_tmp_2" + (Compiler.CompilationModel.Expr.lt + (Compiler.CompilationModel.Expr.localVar "_verity_slice_tmp_1") + (Compiler.CompilationModel.Expr.literal 340282366920938463463374607431768211455)), + Compiler.CompilationModel.Stmt.letVar + "_verity_slice_tmp_11" + (Compiler.CompilationModel.Expr.literal 0), + Compiler.CompilationModel.Stmt.ite + (Compiler.CompilationModel.Expr.localVar "_verity_slice_tmp_2") + [Compiler.CompilationModel.Stmt.letVar + "_verity_slice_tmp_3" + (Compiler.CompilationModel.Expr.structMember + "marketState" + (Compiler.CompilationModel.Expr.param "id") + "lossFactor"), + Compiler.CompilationModel.Stmt.letVar + "_verity_slice_tmp_4" + (Compiler.CompilationModel.Expr.literal 0), + Compiler.CompilationModel.Stmt.ite + (Compiler.CompilationModel.Expr.lt + (Compiler.CompilationModel.Expr.literal 340282366920938463463374607431768211455) + (Compiler.CompilationModel.Expr.localVar "_verity_slice_tmp_3")) + [Compiler.CompilationModel.Stmt.panic (Verity.Core.PanicCode.arithmeticOverflow)] + [Compiler.CompilationModel.Stmt.assignVar + "_verity_slice_tmp_4" + (Compiler.CompilationModel.Expr.sub + (Compiler.CompilationModel.Expr.literal 340282366920938463463374607431768211455) + (Compiler.CompilationModel.Expr.localVar "_verity_slice_tmp_3"))], + Compiler.CompilationModel.Stmt.letVar + "_verity_slice_tmp_6" + (Compiler.CompilationModel.Expr.localVar "_verity_slice_tmp_4"), + Compiler.CompilationModel.Stmt.letVar + "_verity_slice_tmp_5" + (Compiler.CompilationModel.Expr.literal 0), + Compiler.CompilationModel.Stmt.ite + (Compiler.CompilationModel.Expr.lt + (Compiler.CompilationModel.Expr.literal 340282366920938463463374607431768211455) + (Compiler.CompilationModel.Expr.localVar "_verity_slice_tmp_1")) + [Compiler.CompilationModel.Stmt.panic (Verity.Core.PanicCode.arithmeticOverflow)] + [Compiler.CompilationModel.Stmt.assignVar + "_verity_slice_tmp_5" + (Compiler.CompilationModel.Expr.sub + (Compiler.CompilationModel.Expr.literal 340282366920938463463374607431768211455) + (Compiler.CompilationModel.Expr.localVar "_verity_slice_tmp_1"))], + Compiler.CompilationModel.Stmt.letVar + "_verity_slice_tmp_7" + (Compiler.CompilationModel.Expr.localVar "_verity_slice_tmp_5"), + Compiler.CompilationModel.Stmt.letVar + "_verity_slice_tmp_8" + (Compiler.CompilationModel.Expr.literal 0), + Compiler.CompilationModel.Stmt.ite + (Compiler.CompilationModel.Expr.eq + (Compiler.CompilationModel.Expr.localVar "_verity_slice_tmp_0") + (Compiler.CompilationModel.Expr.literal 0)) + [Compiler.CompilationModel.Stmt.assignVar + "_verity_slice_tmp_8" + (Compiler.CompilationModel.Expr.mul + (Compiler.CompilationModel.Expr.localVar "_verity_slice_tmp_0") + (Compiler.CompilationModel.Expr.localVar "_verity_slice_tmp_6"))] + [Compiler.CompilationModel.Stmt.ite + (Compiler.CompilationModel.Expr.eq + (Compiler.CompilationModel.Expr.div + (Compiler.CompilationModel.Expr.mul + (Compiler.CompilationModel.Expr.localVar "_verity_slice_tmp_0") + (Compiler.CompilationModel.Expr.localVar "_verity_slice_tmp_6")) + (Compiler.CompilationModel.Expr.localVar "_verity_slice_tmp_0")) + (Compiler.CompilationModel.Expr.localVar "_verity_slice_tmp_6")) + [Compiler.CompilationModel.Stmt.assignVar + "_verity_slice_tmp_8" + (Compiler.CompilationModel.Expr.mul + (Compiler.CompilationModel.Expr.localVar "_verity_slice_tmp_0") + (Compiler.CompilationModel.Expr.localVar "_verity_slice_tmp_6"))] + [Compiler.CompilationModel.Stmt.panic (Verity.Core.PanicCode.arithmeticOverflow)]], + Compiler.CompilationModel.Stmt.letVar + "_verity_slice_tmp_9" + (Compiler.CompilationModel.Expr.localVar "_verity_slice_tmp_8"), + Compiler.CompilationModel.Stmt.letVar + "_verity_slice_tmp_10" + (Compiler.CompilationModel.Expr.literal 0), + Compiler.CompilationModel.Stmt.ite + (Compiler.CompilationModel.Expr.eq + (Compiler.CompilationModel.Expr.localVar "_verity_slice_tmp_7") + (Compiler.CompilationModel.Expr.literal 0)) + [Compiler.CompilationModel.Stmt.panic (Verity.Core.PanicCode.divisionByZero)] + [Compiler.CompilationModel.Stmt.assignVar + "_verity_slice_tmp_10" + (Compiler.CompilationModel.Expr.div + (Compiler.CompilationModel.Expr.localVar "_verity_slice_tmp_9") + (Compiler.CompilationModel.Expr.localVar "_verity_slice_tmp_7"))], + Compiler.CompilationModel.Stmt.assignVar + "_verity_slice_tmp_11" + (Compiler.CompilationModel.Expr.localVar "_verity_slice_tmp_10")] + [Compiler.CompilationModel.Stmt.assignVar + "_verity_slice_tmp_11" + (Compiler.CompilationModel.Expr.literal 0)], + Compiler.CompilationModel.Stmt.letVar + "_verity_slice_tmp_12" + (Compiler.CompilationModel.Expr.localVar "_verity_slice_tmp_11"), + Compiler.CompilationModel.Stmt.letVar + "_verity_slice_tmp_13" + (Compiler.CompilationModel.Expr.structMember2 + "position" + (Compiler.CompilationModel.Expr.param "id") + (Compiler.CompilationModel.Expr.param "user") + "pendingFee"), + Compiler.CompilationModel.Stmt.letVar + "_verity_slice_tmp_14" + (Compiler.CompilationModel.Expr.gt + (Compiler.CompilationModel.Expr.localVar "_verity_slice_tmp_0") + (Compiler.CompilationModel.Expr.literal 0)), + Compiler.CompilationModel.Stmt.letVar + "_verity_slice_tmp_26" + (Compiler.CompilationModel.Expr.literal 0), + Compiler.CompilationModel.Stmt.ite + (Compiler.CompilationModel.Expr.localVar "_verity_slice_tmp_14") + [Compiler.CompilationModel.Stmt.letVar + "_verity_slice_tmp_15" + (Compiler.CompilationModel.Expr.literal 0), + Compiler.CompilationModel.Stmt.ite + (Compiler.CompilationModel.Expr.lt + (Compiler.CompilationModel.Expr.localVar "_verity_slice_tmp_0") + (Compiler.CompilationModel.Expr.localVar "_verity_slice_tmp_12")) + [Compiler.CompilationModel.Stmt.panic (Verity.Core.PanicCode.arithmeticOverflow)] + [Compiler.CompilationModel.Stmt.assignVar + "_verity_slice_tmp_15" + (Compiler.CompilationModel.Expr.sub + (Compiler.CompilationModel.Expr.localVar "_verity_slice_tmp_0") + (Compiler.CompilationModel.Expr.localVar "_verity_slice_tmp_12"))], + Compiler.CompilationModel.Stmt.letVar + "_verity_slice_tmp_16" + (Compiler.CompilationModel.Expr.localVar "_verity_slice_tmp_15"), + Compiler.CompilationModel.Stmt.letVar + "_verity_slice_tmp_17" + (Compiler.CompilationModel.Expr.literal 0), + Compiler.CompilationModel.Stmt.ite + (Compiler.CompilationModel.Expr.eq + (Compiler.CompilationModel.Expr.localVar "_verity_slice_tmp_13") + (Compiler.CompilationModel.Expr.literal 0)) + [Compiler.CompilationModel.Stmt.assignVar + "_verity_slice_tmp_17" + (Compiler.CompilationModel.Expr.mul + (Compiler.CompilationModel.Expr.localVar "_verity_slice_tmp_13") + (Compiler.CompilationModel.Expr.localVar "_verity_slice_tmp_16"))] + [Compiler.CompilationModel.Stmt.ite + (Compiler.CompilationModel.Expr.eq + (Compiler.CompilationModel.Expr.div + (Compiler.CompilationModel.Expr.mul + (Compiler.CompilationModel.Expr.localVar "_verity_slice_tmp_13") + (Compiler.CompilationModel.Expr.localVar "_verity_slice_tmp_16")) + (Compiler.CompilationModel.Expr.localVar "_verity_slice_tmp_13")) + (Compiler.CompilationModel.Expr.localVar "_verity_slice_tmp_16")) + [Compiler.CompilationModel.Stmt.assignVar + "_verity_slice_tmp_17" + (Compiler.CompilationModel.Expr.mul + (Compiler.CompilationModel.Expr.localVar "_verity_slice_tmp_13") + (Compiler.CompilationModel.Expr.localVar "_verity_slice_tmp_16"))] + [Compiler.CompilationModel.Stmt.panic (Verity.Core.PanicCode.arithmeticOverflow)]], + Compiler.CompilationModel.Stmt.letVar + "_verity_slice_tmp_19" + (Compiler.CompilationModel.Expr.localVar "_verity_slice_tmp_17"), + Compiler.CompilationModel.Stmt.letVar + "_verity_slice_tmp_18" + (Compiler.CompilationModel.Expr.literal 0), + Compiler.CompilationModel.Stmt.ite + (Compiler.CompilationModel.Expr.lt + (Compiler.CompilationModel.Expr.localVar "_verity_slice_tmp_0") + (Compiler.CompilationModel.Expr.literal 1)) + [Compiler.CompilationModel.Stmt.panic (Verity.Core.PanicCode.arithmeticOverflow)] + [Compiler.CompilationModel.Stmt.assignVar + "_verity_slice_tmp_18" + (Compiler.CompilationModel.Expr.sub + (Compiler.CompilationModel.Expr.localVar "_verity_slice_tmp_0") + (Compiler.CompilationModel.Expr.literal 1))], + Compiler.CompilationModel.Stmt.letVar + "_verity_slice_tmp_20" + (Compiler.CompilationModel.Expr.localVar "_verity_slice_tmp_18"), + Compiler.CompilationModel.Stmt.letVar + "_verity_slice_tmp_21" + (Compiler.CompilationModel.Expr.literal 0), + Compiler.CompilationModel.Stmt.ite + (Compiler.CompilationModel.Expr.lt + (Compiler.CompilationModel.Expr.add + (Compiler.CompilationModel.Expr.localVar "_verity_slice_tmp_19") + (Compiler.CompilationModel.Expr.localVar "_verity_slice_tmp_20")) + (Compiler.CompilationModel.Expr.localVar "_verity_slice_tmp_19")) + [Compiler.CompilationModel.Stmt.panic (Verity.Core.PanicCode.arithmeticOverflow)] + [Compiler.CompilationModel.Stmt.assignVar + "_verity_slice_tmp_21" + (Compiler.CompilationModel.Expr.add + (Compiler.CompilationModel.Expr.localVar "_verity_slice_tmp_19") + (Compiler.CompilationModel.Expr.localVar "_verity_slice_tmp_20"))], + Compiler.CompilationModel.Stmt.letVar + "_verity_slice_tmp_22" + (Compiler.CompilationModel.Expr.localVar "_verity_slice_tmp_21"), + Compiler.CompilationModel.Stmt.letVar + "_verity_slice_tmp_23" + (Compiler.CompilationModel.Expr.literal 0), + Compiler.CompilationModel.Stmt.ite + (Compiler.CompilationModel.Expr.eq + (Compiler.CompilationModel.Expr.localVar "_verity_slice_tmp_0") + (Compiler.CompilationModel.Expr.literal 0)) + [Compiler.CompilationModel.Stmt.panic (Verity.Core.PanicCode.divisionByZero)] + [Compiler.CompilationModel.Stmt.assignVar + "_verity_slice_tmp_23" + (Compiler.CompilationModel.Expr.div + (Compiler.CompilationModel.Expr.localVar "_verity_slice_tmp_22") + (Compiler.CompilationModel.Expr.localVar "_verity_slice_tmp_0"))], + Compiler.CompilationModel.Stmt.letVar + "_verity_slice_tmp_24" + (Compiler.CompilationModel.Expr.localVar "_verity_slice_tmp_23"), + Compiler.CompilationModel.Stmt.letVar + "_verity_slice_tmp_25" + (Compiler.CompilationModel.Expr.literal 0), + Compiler.CompilationModel.Stmt.ite + (Compiler.CompilationModel.Expr.lt + (Compiler.CompilationModel.Expr.localVar "_verity_slice_tmp_13") + (Compiler.CompilationModel.Expr.localVar "_verity_slice_tmp_24")) + [Compiler.CompilationModel.Stmt.panic (Verity.Core.PanicCode.arithmeticOverflow)] + [Compiler.CompilationModel.Stmt.assignVar + "_verity_slice_tmp_25" + (Compiler.CompilationModel.Expr.sub + (Compiler.CompilationModel.Expr.localVar "_verity_slice_tmp_13") + (Compiler.CompilationModel.Expr.localVar "_verity_slice_tmp_24"))], + Compiler.CompilationModel.Stmt.assignVar + "_verity_slice_tmp_26" + (Compiler.CompilationModel.Expr.localVar "_verity_slice_tmp_25")] + [Compiler.CompilationModel.Stmt.assignVar + "_verity_slice_tmp_26" + (Compiler.CompilationModel.Expr.literal 0)], + Compiler.CompilationModel.Stmt.letVar + "_verity_slice_tmp_27" + (Compiler.CompilationModel.Expr.localVar "_verity_slice_tmp_26"), + Compiler.CompilationModel.Stmt.letVar + "_verity_slice_tmp_28" + (Compiler.CompilationModel.Expr.bitXor + (Compiler.CompilationModel.Expr.blockTimestamp) + (Compiler.CompilationModel.Expr.mul + (Compiler.CompilationModel.Expr.bitXor + (Compiler.CompilationModel.Expr.blockTimestamp) + (Compiler.CompilationModel.Expr.param "m_maturity")) + (Compiler.CompilationModel.Expr.lt + (Compiler.CompilationModel.Expr.param "m_maturity") + (Compiler.CompilationModel.Expr.blockTimestamp)))), + Compiler.CompilationModel.Stmt.letVar + "_verity_slice_tmp_29" + (Compiler.CompilationModel.Expr.structMember2 + "position" + (Compiler.CompilationModel.Expr.param "id") + (Compiler.CompilationModel.Expr.param "user") + "lastAccrual"), + Compiler.CompilationModel.Stmt.letVar + "_verity_slice_tmp_30" + (Compiler.CompilationModel.Expr.lt + (Compiler.CompilationModel.Expr.localVar "_verity_slice_tmp_29") + (Compiler.CompilationModel.Expr.param "m_maturity")), + Compiler.CompilationModel.Stmt.letVar + "_verity_slice_tmp_40" + (Compiler.CompilationModel.Expr.literal 0), + Compiler.CompilationModel.Stmt.ite + (Compiler.CompilationModel.Expr.localVar "_verity_slice_tmp_30") + [Compiler.CompilationModel.Stmt.letVar + "_verity_slice_tmp_31" + (Compiler.CompilationModel.Expr.literal 0), + Compiler.CompilationModel.Stmt.ite + (Compiler.CompilationModel.Expr.lt + (Compiler.CompilationModel.Expr.localVar "_verity_slice_tmp_28") + (Compiler.CompilationModel.Expr.localVar "_verity_slice_tmp_29")) + [Compiler.CompilationModel.Stmt.panic (Verity.Core.PanicCode.arithmeticOverflow)] + [Compiler.CompilationModel.Stmt.assignVar + "_verity_slice_tmp_31" + (Compiler.CompilationModel.Expr.sub + (Compiler.CompilationModel.Expr.localVar "_verity_slice_tmp_28") + (Compiler.CompilationModel.Expr.localVar "_verity_slice_tmp_29"))], + Compiler.CompilationModel.Stmt.letVar + "_verity_slice_tmp_33" + (Compiler.CompilationModel.Expr.localVar "_verity_slice_tmp_31"), + Compiler.CompilationModel.Stmt.letVar + "_verity_slice_tmp_32" + (Compiler.CompilationModel.Expr.literal 0), + Compiler.CompilationModel.Stmt.ite + (Compiler.CompilationModel.Expr.lt + (Compiler.CompilationModel.Expr.param "m_maturity") + (Compiler.CompilationModel.Expr.localVar "_verity_slice_tmp_29")) + [Compiler.CompilationModel.Stmt.panic (Verity.Core.PanicCode.arithmeticOverflow)] + [Compiler.CompilationModel.Stmt.assignVar + "_verity_slice_tmp_32" + (Compiler.CompilationModel.Expr.sub + (Compiler.CompilationModel.Expr.param "m_maturity") + (Compiler.CompilationModel.Expr.localVar "_verity_slice_tmp_29"))], + Compiler.CompilationModel.Stmt.letVar + "_verity_slice_tmp_34" + (Compiler.CompilationModel.Expr.localVar "_verity_slice_tmp_32"), + Compiler.CompilationModel.Stmt.letVar + "_verity_slice_tmp_35" + (Compiler.CompilationModel.Expr.literal 0), + Compiler.CompilationModel.Stmt.ite + (Compiler.CompilationModel.Expr.eq + (Compiler.CompilationModel.Expr.localVar "_verity_slice_tmp_27") + (Compiler.CompilationModel.Expr.literal 0)) + [Compiler.CompilationModel.Stmt.assignVar + "_verity_slice_tmp_35" + (Compiler.CompilationModel.Expr.mul + (Compiler.CompilationModel.Expr.localVar "_verity_slice_tmp_27") + (Compiler.CompilationModel.Expr.localVar "_verity_slice_tmp_33"))] + [Compiler.CompilationModel.Stmt.ite + (Compiler.CompilationModel.Expr.eq + (Compiler.CompilationModel.Expr.div + (Compiler.CompilationModel.Expr.mul + (Compiler.CompilationModel.Expr.localVar "_verity_slice_tmp_27") + (Compiler.CompilationModel.Expr.localVar "_verity_slice_tmp_33")) + (Compiler.CompilationModel.Expr.localVar "_verity_slice_tmp_27")) + (Compiler.CompilationModel.Expr.localVar "_verity_slice_tmp_33")) + [Compiler.CompilationModel.Stmt.assignVar + "_verity_slice_tmp_35" + (Compiler.CompilationModel.Expr.mul + (Compiler.CompilationModel.Expr.localVar "_verity_slice_tmp_27") + (Compiler.CompilationModel.Expr.localVar "_verity_slice_tmp_33"))] + [Compiler.CompilationModel.Stmt.panic (Verity.Core.PanicCode.arithmeticOverflow)]], + Compiler.CompilationModel.Stmt.letVar + "_verity_slice_tmp_36" + (Compiler.CompilationModel.Expr.localVar "_verity_slice_tmp_35"), + Compiler.CompilationModel.Stmt.letVar + "_verity_slice_tmp_37" + (Compiler.CompilationModel.Expr.literal 0), + Compiler.CompilationModel.Stmt.ite + (Compiler.CompilationModel.Expr.eq + (Compiler.CompilationModel.Expr.localVar "_verity_slice_tmp_34") + (Compiler.CompilationModel.Expr.literal 0)) + [Compiler.CompilationModel.Stmt.panic (Verity.Core.PanicCode.divisionByZero)] + [Compiler.CompilationModel.Stmt.assignVar + "_verity_slice_tmp_37" + (Compiler.CompilationModel.Expr.div + (Compiler.CompilationModel.Expr.localVar "_verity_slice_tmp_36") + (Compiler.CompilationModel.Expr.localVar "_verity_slice_tmp_34"))], + Compiler.CompilationModel.Stmt.letVar + "_verity_slice_tmp_38" + (Compiler.CompilationModel.Expr.localVar "_verity_slice_tmp_37"), + Compiler.CompilationModel.Stmt.letVar + "_verity_slice_tmp_39" + (Compiler.CompilationModel.Expr.bitAnd + (Compiler.CompilationModel.Expr.localVar "_verity_slice_tmp_38") + (Compiler.CompilationModel.Expr.literal 340282366920938463463374607431768211455)), + Compiler.CompilationModel.Stmt.assignVar + "_verity_slice_tmp_40" + (Compiler.CompilationModel.Expr.localVar "_verity_slice_tmp_39")] + [Compiler.CompilationModel.Stmt.assignVar + "_verity_slice_tmp_40" + (Compiler.CompilationModel.Expr.literal 0)], + Compiler.CompilationModel.Stmt.letVar + "_verity_slice_tmp_41" + (Compiler.CompilationModel.Expr.localVar "_verity_slice_tmp_40"), + Compiler.CompilationModel.Stmt.letVar + "_verity_slice_tmp_42" + (Compiler.CompilationModel.Expr.bitAnd + (Compiler.CompilationModel.Expr.localVar "_verity_slice_tmp_12") + (Compiler.CompilationModel.Expr.literal 340282366920938463463374607431768211455)), + Compiler.CompilationModel.Stmt.letVar + "_verity_slice_tmp_43" + (Compiler.CompilationModel.Expr.localVar "_verity_slice_tmp_42"), + Compiler.CompilationModel.Stmt.letVar + "_verity_slice_tmp_44" + (Compiler.CompilationModel.Expr.literal 0), + Compiler.CompilationModel.Stmt.ite + (Compiler.CompilationModel.Expr.lt + (Compiler.CompilationModel.Expr.localVar "_verity_slice_tmp_43") + (Compiler.CompilationModel.Expr.localVar "_verity_slice_tmp_41")) + [Compiler.CompilationModel.Stmt.panic (Verity.Core.PanicCode.arithmeticOverflow)] + [Compiler.CompilationModel.Stmt.assignVar + "_verity_slice_tmp_44" + (Compiler.CompilationModel.Expr.sub + (Compiler.CompilationModel.Expr.localVar "_verity_slice_tmp_43") + (Compiler.CompilationModel.Expr.localVar "_verity_slice_tmp_41"))], + Compiler.CompilationModel.Stmt.letVar + "_verity_slice_tmp_45" + (Compiler.CompilationModel.Expr.localVar "_verity_slice_tmp_44"), + Compiler.CompilationModel.Stmt.letVar + "_verity_slice_tmp_46" + (Compiler.CompilationModel.Expr.bitAnd + (Compiler.CompilationModel.Expr.localVar "_verity_slice_tmp_27") + (Compiler.CompilationModel.Expr.literal 340282366920938463463374607431768211455)), + Compiler.CompilationModel.Stmt.letVar + "_verity_slice_tmp_47" + (Compiler.CompilationModel.Expr.localVar "_verity_slice_tmp_46"), + Compiler.CompilationModel.Stmt.letVar + "_verity_slice_tmp_48" + (Compiler.CompilationModel.Expr.literal 0), + Compiler.CompilationModel.Stmt.ite + (Compiler.CompilationModel.Expr.lt + (Compiler.CompilationModel.Expr.localVar "_verity_slice_tmp_47") + (Compiler.CompilationModel.Expr.localVar "_verity_slice_tmp_41")) + [Compiler.CompilationModel.Stmt.panic (Verity.Core.PanicCode.arithmeticOverflow)] + [Compiler.CompilationModel.Stmt.assignVar + "_verity_slice_tmp_48" + (Compiler.CompilationModel.Expr.sub + (Compiler.CompilationModel.Expr.localVar "_verity_slice_tmp_47") + (Compiler.CompilationModel.Expr.localVar "_verity_slice_tmp_41"))], + Compiler.CompilationModel.Stmt.letVar + "_verity_slice_tmp_49" + (Compiler.CompilationModel.Expr.localVar "_verity_slice_tmp_48"), + Compiler.CompilationModel.Stmt.returnValues + [Compiler.CompilationModel.Expr.localVar "_verity_slice_tmp_45", + Compiler.CompilationModel.Expr.localVar "_verity_slice_tmp_49", + Compiler.CompilationModel.Expr.localVar "_verity_slice_tmp_41"]], + modifies := [], + noExternalCalls := false, + allowPostInteractionWrites := false, + nonReentrantLock := none, + ceiSafe := false, + reentrancyTrusted := false, + requiresRole := none, + isInternal := false, + localObligations := [] }], + events := [], + errors := [], + externals := [], + adtTypes := [], + storageNamespace := none } diff --git a/scripts/solidity_slice_mutations.py b/scripts/solidity_slice_mutations.py index eb5606552b..c4ce78fd5e 100644 --- a/scripts/solidity_slice_mutations.py +++ b/scripts/solidity_slice_mutations.py @@ -17,6 +17,7 @@ SOLC = ROOT / ".lake/solidity-import/solc-0.8.34" WORK = ROOT / ".lake/slice-mutations" SMOKE = ROOT / "Contracts/SoliditySliceSmoke" +GOLDEN = SMOKE / "model.golden" HEADER = """ import Compiler.SoliditySlice.Import @@ -258,7 +259,16 @@ def main() -> None: if not SOLC.is_file(): raise SystemExit(f"missing {SOLC}") WORK.mkdir(parents=True, exist_ok=True) - base = expect_success("unreached-for", write_project("base", unchanged), witness=True) + snapshot = WORK / "model.txt" + snapshot.unlink(missing_ok=True) + base = expect_success("unreached-for", write_project("base", unchanged), witness=True, + extra=f'\n#eval IO.FS.writeFile "{snapshot}" (toString (repr imported.model) ++ "\\n")\n') + # Refactors of the importer must reproduce the imported model exactly. + if "--update-golden" in sys.argv: + shutil.copy(snapshot, GOLDEN) + elif snapshot.read_text() != GOLDEN.read_text(): + raise SystemExit(f"imported model differs from {GOLDEN}; review, then rerun with --update-golden") + print("pass golden-model") expect_success("helper-local-hygiene", write_project("helper-local", helper_local_collision), witness=True) expect_success("generated-name-hygiene", write_project("generated-name", generated_name_collision), witness=True) expect_success("unrelated-layout", write_project("unrelated-layout", unrelated_layout), witness=True) From 394d8b4694d9177a717d4e3dd204fe18c16f2db7 Mon Sep 17 00:00:00 2001 From: Thomas Marchand Date: Thu, 24 Sep 2026 15:32:05 +0200 Subject: [PATCH 12/22] Rename the slice importer to Solidity import with idiomatic syntax MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Compiler/SoliditySlice becomes Compiler/SolidityImport and the positional `solidity_slice_import … slice_bytecode_hash "none"` command becomes: solidity_import x from "" entry "" using contract C function f(Market, bytes32, address) The solc settings are an ordinary `Profile` value (named or inline), the function is selected by its Solidity parameter types, and the generated kernel check is `x.covered`. Report types become ImportReport, ImportedFunction and ParamProjection; scripts become setup_solc_import.py, solidity_import_mutations.py and solidity_import_differential.py. The imported smoke model is byte-identical to model.golden. --- .github/workflows/verify.yml | 36 ++-- AUDIT.md | 16 +- AXIOMS.md | 6 +- .../Coverage.lean | 8 +- .../Differential.lean | 12 +- .../Import.lean | 189 +++++++++++------- Compiler/SolidityImport/Profile.lean | 27 +++ .../README.md | 38 ++-- .../Report.lean | 18 +- .../TESTING.md | 2 +- .../Lib.sol | 0 .../Slice.sol | 0 .../Smoke.lean | 26 +-- .../differential-cases.json | 0 .../differential.json | 0 .../model.golden | 0 TRUST_ASSUMPTIONS.md | 4 +- artifacts/trust_surface_report.json | 2 +- lakefile.lean | 4 +- scripts/check_solidity_differential.sh | 4 +- ...tup_solc_slice.py => setup_solc_import.py} | 0 scripts/solidity_differential/engine.py | 23 ++- scripts/solidity_differential/mutations.py | 8 +- ...ial.py => solidity_import_differential.py} | 0 ...ations.py => solidity_import_mutations.py} | 32 ++- scripts/verify_sync_spec.json | 22 +- scripts/verify_sync_spec_source.py | 12 +- 27 files changed, 278 insertions(+), 211 deletions(-) rename Compiler/{SoliditySlice => SolidityImport}/Coverage.lean (98%) rename Compiler/{SoliditySlice => SolidityImport}/Differential.lean (92%) rename Compiler/{SoliditySlice => SolidityImport}/Import.lean (89%) create mode 100644 Compiler/SolidityImport/Profile.lean rename Compiler/{SoliditySlice => SolidityImport}/README.md (77%) rename Compiler/{SoliditySlice => SolidityImport}/Report.lean (87%) rename Compiler/{SoliditySlice => SolidityImport}/TESTING.md (99%) rename Contracts/{SoliditySliceSmoke => SolidityImportSmoke}/Lib.sol (100%) rename Contracts/{SoliditySliceSmoke => SolidityImportSmoke}/Slice.sol (100%) rename Contracts/{SoliditySliceSmoke => SolidityImportSmoke}/Smoke.lean (83%) rename Contracts/{SoliditySliceSmoke => SolidityImportSmoke}/differential-cases.json (100%) rename Contracts/{SoliditySliceSmoke => SolidityImportSmoke}/differential.json (100%) rename Contracts/{SoliditySliceSmoke => SolidityImportSmoke}/model.golden (100%) rename scripts/{setup_solc_slice.py => setup_solc_import.py} (100%) rename scripts/{solidity_slice_differential.py => solidity_import_differential.py} (100%) rename scripts/{solidity_slice_mutations.py => solidity_import_mutations.py} (92%) diff --git a/.github/workflows/verify.yml b/.github/workflows/verify.yml index b0d6977d77..74b7cd384d 100644 --- a/.github/workflows/verify.yml +++ b/.github/workflows/verify.yml @@ -232,10 +232,10 @@ jobs: - 'lakefile.lean' - 'lake-manifest.json' - 'scripts/solidity_differential/**' - - 'scripts/solidity_slice_differential.py' + - 'scripts/solidity_import_differential.py' - 'scripts/check_solidity_differential.sh' - - 'scripts/solidity_slice_mutations.py' - - 'scripts/setup_solc_slice.py' + - 'scripts/solidity_import_mutations.py' + - 'scripts/setup_solc_import.py' - 'lean-toolchain' compiler: - '.github/actions/**' @@ -261,10 +261,10 @@ jobs: - 'lakefile.lean' - 'lake-manifest.json' - 'scripts/solidity_differential/**' - - 'scripts/solidity_slice_differential.py' + - 'scripts/solidity_import_differential.py' - 'scripts/check_solidity_differential.sh' - - 'scripts/solidity_slice_mutations.py' - - 'scripts/setup_solc_slice.py' + - 'scripts/solidity_import_mutations.py' + - 'scripts/setup_solc_import.py' - 'lean-toolchain' - 'foundry.toml' - name: Resolve effective change flags @@ -431,9 +431,9 @@ jobs: - name: Check Solidity slice imports and mutation regressions run: | - python3 scripts/setup_solc_slice.py - lake build SoliditySliceSmoke - python3 scripts/solidity_slice_mutations.py + python3 scripts/setup_solc_import.py + lake build SolidityImportSmoke + python3 scripts/solidity_import_mutations.py - name: Check storage layout audit artifacts (#1897) run: make verify-storage-layout @@ -1247,26 +1247,26 @@ jobs: - name: Compare Solidity, Denote and compiled models timeout-minutes: 10 run: | - scripts/check_solidity_differential.sh --config Contracts/SoliditySliceSmoke/differential.json --output .lake/slice-differential/smoke --cases 128 --seed 2438 - scripts/check_solidity_differential.sh --programs 5 --cases 16 --seed 2438 --output .lake/slice-differential/programs + scripts/check_solidity_differential.sh --config Contracts/SolidityImportSmoke/differential.json --output .lake/import-differential/smoke --cases 128 --seed 2438 + scripts/check_solidity_differential.sh --programs 5 --cases 16 --seed 2438 --output .lake/import-differential/programs - name: Extended Solidity slice campaigns if: github.event_name == 'schedule' || github.event_name == 'workflow_dispatch' timeout-minutes: 60 run: | - scripts/check_solidity_differential.sh --config Contracts/SoliditySliceSmoke/differential.json --output .lake/slice-differential/nightly-inputs --cases 2048 --seed "$GITHUB_RUN_NUMBER" - scripts/check_solidity_differential.sh --programs 20 --cases 64 --seed "$GITHUB_RUN_NUMBER" --output .lake/slice-differential/nightly-programs - scripts/check_solidity_differential.sh --mutations --output .lake/slice-differential/mutants + scripts/check_solidity_differential.sh --config Contracts/SolidityImportSmoke/differential.json --output .lake/import-differential/nightly-inputs --cases 2048 --seed "$GITHUB_RUN_NUMBER" + scripts/check_solidity_differential.sh --programs 20 --cases 64 --seed "$GITHUB_RUN_NUMBER" --output .lake/import-differential/nightly-programs + scripts/check_solidity_differential.sh --mutations --output .lake/import-differential/mutants - name: Archive Solidity slice comparison evidence if: always() uses: actions/upload-artifact@v7 with: - name: solidity-slice-differential + name: solidity-import-differential path: | - .lake/slice-differential/** - !.lake/slice-differential/**/.lake/build/** - !.lake/slice-differential/**/.lake/packages/** + .lake/import-differential/** + !.lake/import-differential/**/.lake/build/** + !.lake/import-differential/**/.lake/packages/** include-hidden-files: true retention-days: 14 diff --git a/AUDIT.md b/AUDIT.md index 7a712426fa..a1667758da 100644 --- a/AUDIT.md +++ b/AUDIT.md @@ -7,25 +7,25 @@ boundary checks change. ## Solidity function-slice import -`Compiler/SoliditySlice` lowers one selected function and the callees solc's +`Compiler/SolidityImport` lowers one selected function and the callees solc's declaration ids reach into a `CompilationModel`. `returnValues` is observable on `DenoteState.observedReturnWords` and is mirrored in `SourceSemantics` and -`DenoteAgreement`. Accepted slices carry `sliceCovered`, proved by `decide` -against `modelSliceCovered`. The smoke contract -`Contracts/SoliditySliceSmoke` contains an unreached `for` loop; the import +`DenoteAgreement`. Accepted imports carry `covered`, proved by `decide` +against `modelImportCovered`. The smoke contract +`Contracts/SolidityImportSmoke` contains an unreached `for` loop; the import still succeeds. The mutation harness checks the interpreter result of two witnesses on that imported body: `(95, 5, 5)` and a revert. `smoke_step_credit` is a kernel proof of the first packed `credit` read. It is not a proof of the whole body, and it is not a proof that the model matches solc bytecode. -The slice compiler pin is solc `0.8.34+commit.80d5c536`, installed by `python3 scripts/setup_solc_slice.py`. The source +The slice compiler pin is solc `0.8.34+commit.80d5c536`, installed by `python3 scripts/setup_solc_import.py`. The source digest reads the importer modules from the Verity tree, or from `.lake/packages/verity` when a downstream package elaborates the command. The solc binary remains the elaborating package's `.lake/solidity-import/solc-0.8.34`. -Evidence commands: `lake build SoliditySliceSmoke` after the 0.8.34 compiler is +Evidence commands: `lake build SolidityImportSmoke` after the 0.8.34 compiler is installed at `.lake/solidity-import/solc-0.8.34`, and -`python3 scripts/solidity_slice_mutations.py`. The mutation script checks an +`python3 scripts/solidity_import_mutations.py`. The mutation script checks an unreached `for`, rejection of that `for` once it is reachable, and detection of a reached-helper change, a fee-factor change, a field change, a return-order change, and a layout swap. It also covers hygienic helper locals, generated-name @@ -36,7 +36,7 @@ A rejected import leaves no `.olean`. The downstream `morpho-midnight-verity` pilot proves all five CVL assertions on actual imported execution, with arbitrary state/oracle and mathematical integer ghosts. That proof adds no source-to-model equivalence claim. See -`Compiler/SoliditySlice/README.md` for the supported subset and freshness checks. +`Compiler/SolidityImport/README.md` for the supported subset and freshness checks. ## Current Audit State diff --git a/AXIOMS.md b/AXIOMS.md index 7bafd04a33..d0c74c9f3e 100644 --- a/AXIOMS.md +++ b/AXIOMS.md @@ -2,10 +2,10 @@ This file is the authoritative registry of axioms used by Verity proof code. -## Solidity function-slice import +## Solidity import -`slice.sliceCovered` and `smoke_step_credit` are ordinary theorems. The slice -path adds no project axiom, `sorry`, or `native_decide`. `sliceCovered` is +`smoke.covered` and `smoke_step_credit` are ordinary theorems. The import +path adds no project axiom, `sorry`, or `native_decide`. `covered` is `decide` on the whitelist. `smoke_step_credit` is `simp` plus `decide` on the concrete packed read. Interpreter `#eval` checks are not theorems. The frontend that builds the model from solc JSON remains a trust assumption; see diff --git a/Compiler/SoliditySlice/Coverage.lean b/Compiler/SolidityImport/Coverage.lean similarity index 98% rename from Compiler/SoliditySlice/Coverage.lean rename to Compiler/SolidityImport/Coverage.lean index cec6f85d4c..9fe47a0c12 100644 --- a/Compiler/SoliditySlice/Coverage.lean +++ b/Compiler/SolidityImport/Coverage.lean @@ -3,7 +3,7 @@ import Verity.Core.Model.Denote /-! # Denotation coverage certificate for Solidity slices -The Solidity-slice importer (`Compiler/SoliditySlice`) emits only a small, +The Solidity-slice importer (`Compiler/SolidityImport`) emits only a small, explicitly listed fragment of the deep `CompilationModel`. This module is the machine-checked side of that contract: @@ -24,7 +24,7 @@ the Denote↔IR raccord boundary (it rejects `panic`, `returnValues`, ...), while this one characterizes the compiler-free `Denote` execution path only. -/ -namespace Compiler.CompilationModel.SoliditySlice +namespace Compiler.CompilationModel.SolidityImport open Compiler.CompilationModel open Compiler.CompilationModel.Denote @@ -298,7 +298,7 @@ theorem execStmtList_returnValues_stop_arm (oracle : DenoteOracle) (fields : Lis simp [execStmtList, execStmt, evalExprList, evalExpr, wordNormalize] /-- Every function body of a model is in the slice whitelist. -/ -def modelSliceCovered (model : CompilationModel) : Bool := +def modelImportCovered (model : CompilationModel) : Bool := model.functions.all fun fn => stmtListCovered fn.body /-- Level A: run a body on scalar bindings. Arguments are Solidity words, not @@ -397,4 +397,4 @@ theorem execStmtList_slice_storageWords intro state exact execStmtList_slice_world oracle fields state body h -end Compiler.CompilationModel.SoliditySlice +end Compiler.CompilationModel.SolidityImport diff --git a/Compiler/SoliditySlice/Differential.lean b/Compiler/SolidityImport/Differential.lean similarity index 92% rename from Compiler/SoliditySlice/Differential.lean rename to Compiler/SolidityImport/Differential.lean index dd1c112e90..c82d053bd9 100644 --- a/Compiler/SoliditySlice/Differential.lean +++ b/Compiler/SolidityImport/Differential.lean @@ -1,5 +1,5 @@ -import Compiler.SoliditySlice.Coverage -import Compiler.SoliditySlice.Report +import Compiler.SolidityImport.Coverage +import Compiler.SolidityImport.Report import Compiler.CompilationModel import Compiler.Codegen import Compiler.Yul.PrettyPrint @@ -9,7 +9,7 @@ import Lean.Data.Json /-! Test-only runner for an arbitrary covered Solidity slice. The execution path is the same `Denote.execStmtList` used by proofs. JSON/IO and the concrete hash implementation are harness code, not additional proof assumptions. -/ -namespace Compiler.CompilationModel.SoliditySlice.Differential +namespace Compiler.CompilationModel.SolidityImport.Differential open Lean Compiler.CompilationModel.Denote Verity.Core private def word (j : Json) : Except String Nat := do @@ -68,8 +68,8 @@ private def execute (model : CompilationModel) (fn : FunctionSpec) (j : Json) : /-- The driver imports the selected model, then invokes this shared entrypoint. `describe` also attempts the ordinary Verity compiler; a failure is explicit. -/ -def run (model : CompilationModel) (report : SliceReport) (args : List String) : IO UInt32 := do - unless modelSliceCovered model do throw (IO.userError "unsupported Denote slice") +def run (model : CompilationModel) (report : ImportReport) (args : List String) : IO UInt32 := do + unless modelImportCovered model do throw (IO.userError "unsupported Denote slice") let [fn] := model.functions | throw (IO.userError "expected exactly one imported root") match args with | ["describe", output, yulPath] => @@ -95,4 +95,4 @@ def run (model : CompilationModel) (report : SliceReport) (args : List String) : | _ => throw (IO.userError "expected describe OUTPUT YUL or run INPUT OUTPUT DIGEST") return 0 -end Compiler.CompilationModel.SoliditySlice.Differential +end Compiler.CompilationModel.SolidityImport.Differential diff --git a/Compiler/SoliditySlice/Import.lean b/Compiler/SolidityImport/Import.lean similarity index 89% rename from Compiler/SoliditySlice/Import.lean rename to Compiler/SolidityImport/Import.lean index 7d6e5da787..d9e6cabe78 100644 --- a/Compiler/SoliditySlice/Import.lean +++ b/Compiler/SolidityImport/Import.lean @@ -1,24 +1,35 @@ import Lean import Compiler.Sha256.Engine -import Compiler.SoliditySlice.Coverage -import Compiler.SoliditySlice.Report +import Compiler.SolidityImport.Coverage +import Compiler.SolidityImport.Profile +import Compiler.SolidityImport.Report /-! -Slice importer. Pinned `solc --standard-json` supplies the AST, declaration -ids, and `storageLayout`. This module selects one function, closes over the -definitions that resolution actually reaches, and elaborates a +Solidity importer. Pinned `solc --standard-json` supplies the AST, declaration +ids, and `storageLayout`. The `solidity_import` command selects a function, +closes over the definitions that resolution actually reaches, and elaborates a `CompilationModel`. It does not interpret that model: execution is `Denote.execStmt`, restricted by `stmtListCovered`. +```lean +solidity_import midnight from "vendor/midnight" entry "src/Midnight.sol" + using { evmVersion := "osaka", viaIR := true, optimizerRuns := some 466, bytecodeHash := "none" } + contract Midnight + function updatePositionView(Market, bytes32, address) +``` + +This defines `midnight.model`, `midnight.report`, `midnight.sourceDigest` and +the theorem `midnight.covered`. + The frontend is in the trust base. A covered model is not, by itself, a proof that the model matches the Solidity source or solc's bytecode. -/ open Lean Meta Elab Command -namespace Compiler.CompilationModel.SoliditySlice +namespace Compiler.CompilationModel.SolidityImport -def importerVersion : String := "solidity-slice-3" +def importerVersion : String := "solidity-import-1" private def solcLongVersion := "0.8.34+commit.80d5c536" @@ -211,7 +222,7 @@ private def failAt (j : Json) (why : String) : M α := do let owner := env.funContract.find? id |>.getD "" let name := (env.funs.find? id).bind (fun fn => optStr fn "name") |>.getD s!"decl#{id}" s!"{owner}.{name}" - let why := s!"[solidity-slice:unsupported] {why}\nclosure: {String.intercalate " -> " frames}" + let why := s!"[solidity-import:unsupported] {why}\nclosure: {String.intercalate " -> " frames}" let file := match (field? j "id").bind (fun id => id.getNat?.toOption) with | some id => env.nodeFile.find? id |>.getD env.currentFile @@ -1023,12 +1034,12 @@ private def verifyCompiler (compiler : System.FilePath) : MetaM String := do stores that checkout at `.lake/packages/verity`. The digest must hash those sources in either layout. -/ private def importerSourceRoot (pkgRoot : System.FilePath) : MetaM System.FilePath := do - if ← (pkgRoot / "Compiler/SoliditySlice/Import.lean").pathExists then + if ← (pkgRoot / "Compiler/SolidityImport/Import.lean").pathExists then return pkgRoot let dep := pkgRoot / ".lake/packages/verity" - if ← (dep / "Compiler/SoliditySlice/Import.lean").pathExists then + if ← (dep / "Compiler/SolidityImport/Import.lean").pathExists then return dep - throwError "importer source missing: {pkgRoot / "Compiler/SoliditySlice/Import.lean"}" + throwError "importer source missing: {pkgRoot / "Compiler/SolidityImport/Import.lean"}" private def moduleText (pkgRoot : System.FilePath) (rel : String) : MetaM String := do let path := pkgRoot / rel @@ -1037,11 +1048,27 @@ private def moduleText (pkgRoot : System.FilePath) (rel : String) : MetaM String private def renderFn (f : FnRec) : String := let tys := leanList (f.paramTypes.map leanStr) - s!"({ "{ " }contract := {leanStr f.contract}, name := {leanStr f.name}, declId := {f.declId}, paramTypes := {tys}{ " }" } : Compiler.CompilationModel.SoliditySlice.SliceFunction)" - -private def selectFunction (contract functionName : String) (paramTys : Array String) : M Json := do + s!"({ "{ " }contract := {leanStr f.contract}, name := {leanStr f.name}, declId := {f.declId}, paramTypes := {tys}{ " }" } : Compiler.CompilationModel.SolidityImport.ImportedFunction)" + +/-- Solidity spelling of a solc `typeString`: `struct Market memory` → `Market`. -/ +private def sourceTypeName (typeString : String) : String := Id.run do + let mut t := typeString + for pre in ["struct ", "contract ", "enum "] do + if t.startsWith pre then t := (t.drop pre.length).toString + for suf in [" storage pointer", " storage ref", " memory", " calldata", " storage"] do + if t.endsWith suf then t := (t.dropEnd suf.length).toString + return t + +/-- A written type matches its Solidity spelling, optionally qualified (`I.Market`). -/ +private def typeMatches (written typeString : String) : Bool := + let t := sourceTypeName typeString + t == written || t.endsWith ("." ++ written) || written.endsWith ("." ++ t) + +/-- Select the function; also return its solc parameter type strings. -/ +private def selectFunction (contract functionName : String) (written : Array String) : + M (Json × Array String) := do let env ← get - let mut hits : Array Json := #[] + let mut hits : Array (Json × Array String) := #[] let mut described : Array String := #[] for (id, fn) in env.funs do let name ← mStr (← mField fn "name") @@ -1051,24 +1078,25 @@ private def selectFunction (contract functionName : String) (paramTys : Array St for p in params do tys := tys.push (← mType p) if owner == contract && name == functionName then - described := described.push (String.intercalate ", " tys.toList) - if tys == paramTys then - hits := hits.push fn + described := described.push + s!"{functionName}({String.intercalate ", " (tys.map sourceTypeName).toList})" + if tys.size == written.size && (tys.zip written).all (fun (t, w) => typeMatches w t) then + hits := hits.push (fn, tys) + let sig := s!"{contract}.{functionName}({String.intercalate ", " written.toList})" if hits.size == 0 then - throwError "no function {contract}.{functionName} matching [{String.intercalate ", " paramTys.toList}]; candidates: {described}" + throwError "no function {sig}; candidates: {described}" if hits.size > 1 then - throwError "ambiguous function {contract}.{functionName}" - let fn := hits[0]! + throwError "ambiguous function {sig}; qualify the parameter types" + let (fn, tys) := hits[0]! let implemented ← match field? fn "implemented" with | some b => mBool b | none => pure true unless implemented do throwError "function is not implemented" - pure fn + pure (fn, tys) private def importSlice (pkgRoot projectRoot : System.FilePath) (entry contract functionName : String) - (paramTys : Array String) (viaIR optimizer : Bool) (evm bytecodeHash : String) - (runs : Nat) : MetaM (String × String × String) := do + (written : Array String) (profile : Profile) : MetaM (String × String × String) := do let compiler := pkgRoot / ".lake/solidity-import/solc-0.8.34" let solcSha ← verifyCompiler compiler let versionOut ← IO.Process.output { cmd := compiler.toString, args := #["--version"] } @@ -1084,14 +1112,15 @@ private def importSlice fileBytes := fileBytes.insert logical (text.toUTF8) sourceObj := sourceObj.push (logical, Json.mkObj [("content", Json.str text)]) let settings := Json.mkObj [ - ("evmVersion", Json.str evm), - ("metadata", Json.mkObj [("bytecodeHash", Json.str bytecodeHash)]), - ("optimizer", Json.mkObj [("enabled", Json.bool optimizer), ("runs", runs)]), + ("evmVersion", Json.str profile.evmVersion), + ("metadata", Json.mkObj [("bytecodeHash", Json.str profile.bytecodeHash)]), + ("optimizer", Json.mkObj [("enabled", Json.bool profile.optimizerRuns.isSome), + ("runs", profile.optimizerRuns.getD 200)]), ("outputSelection", Json.mkObj [("*", Json.mkObj [ ("", Json.arr #[Json.str "ast"]), ("*", Json.arr #[Json.str "storageLayout"])])]), ("remappings", Json.arr (remaps.map fun p => Json.str s!"{p.1}={p.2}")), - ("viaIR", Json.bool viaIR)] + ("viaIR", Json.bool profile.viaIR)] let input := Json.mkObj [ ("language", Json.str "Solidity"), ("settings", settings), @@ -1126,7 +1155,7 @@ private def importSlice for item in items do let label ← str (← field item "label") env := { env with layoutItems := env.layoutItems.insert label item } - let fn ← (selectFunction contract functionName paramTys).run' env + let (fn, paramTys) ← (selectFunction contract functionName written).run' env env := { env with currentFile := entry } let ((body, srcParams), env2) ← (lowerRoot fn).run env env := env2 @@ -1194,16 +1223,16 @@ private def importSlice then none else some (leanStr pair.1) | none => #[] projTerms := projTerms.push - s!"({ "{ " }parameter := {leanStr proj.parameter}, member := {leanStr proj.member}, structName := {leanStr proj.structName}, headWord := {proj.headWord}, modelParam := {leanStr proj.modelParam}, ignoredMembers := {leanList ignored}{ " }" } : Compiler.CompilationModel.SoliditySlice.SliceProjection)" + s!"({ "{ " }parameter := {leanStr proj.parameter}, member := {leanStr proj.member}, structName := {leanStr proj.structName}, headWord := {proj.headWord}, modelParam := {leanStr proj.modelParam}, ignoredMembers := {leanList ignored}{ " }" } : Compiler.CompilationModel.SolidityImport.ParamProjection)" let mut opaqueTerms : Array String := #[] for o in env.opaqueMembers do if env.referenced.contains o.field then opaqueTerms := opaqueTerms.push - s!"({ "{ " }field := {leanStr o.field}, name := {leanStr o.name}, solcType := {leanStr o.solcType}, wordOffset := {o.wordOffset}, byteOffset := {o.byteOffset}{ " }" } : Compiler.CompilationModel.SoliditySlice.OpaqueMember)" + s!"({ "{ " }field := {leanStr o.field}, name := {leanStr o.name}, solcType := {leanStr o.solcType}, wordOffset := {o.wordOffset}, byteOffset := {o.byteOffset}{ " }" } : Compiler.CompilationModel.SolidityImport.OpaqueMember)" let sourceRoot ← importerSourceRoot pkgRoot - let importer ← moduleText sourceRoot "Compiler/SoliditySlice/Import.lean" - let coverage ← moduleText sourceRoot "Compiler/SoliditySlice/Coverage.lean" - let reportSrc ← moduleText sourceRoot "Compiler/SoliditySlice/Report.lean" + let importer ← moduleText sourceRoot "Compiler/SolidityImport/Import.lean" + let coverage ← moduleText sourceRoot "Compiler/SolidityImport/Coverage.lean" + let reportSrc ← moduleText sourceRoot "Compiler/SolidityImport/Report.lean" -- JSON framing prevents distinct file/signature lists from sharing a -- concatenation merely because their text contains separator newlines. let digestInput := Json.mkObj [ @@ -1218,66 +1247,74 @@ private def importSlice ("Report.lean", Json.str reportSrc)])] let digest := sha256Hex digestInput.compress.toUTF8 let reportTerm := - s!"({ "{ " }importerVersion := {leanStr importerVersion}, solcLongVersion := {leanStr solcLongVersion}, solcSha256 := {leanStr solcSha}, settingsJson := {leanStr settings.compress}, sourceDigest := {leanStr digest}, contract := {leanStr contract}, rootFunction := {leanStr functionName}, includedFunctions := {leanList (env.included.map renderFn)}, excludedFunctions := {leanList (sortedExcluded.map renderFn)}, projections := {leanList projTerms}, storageFields := {leanList (env.referenced.map leanStr)}, opaqueMembers := {leanList opaqueTerms}, observesPanicPayload := false{ " }" } : Compiler.CompilationModel.SoliditySlice.SliceReport)" + s!"({ "{ " }importerVersion := {leanStr importerVersion}, solcLongVersion := {leanStr solcLongVersion}, solcSha256 := {leanStr solcSha}, settingsJson := {leanStr settings.compress}, sourceDigest := {leanStr digest}, contract := {leanStr contract}, rootFunction := {leanStr functionName}, includedFunctions := {leanList (env.included.map renderFn)}, excludedFunctions := {leanList (sortedExcluded.map renderFn)}, projections := {leanList projTerms}, storageFields := {leanList (env.referenced.map leanStr)}, opaqueMembers := {leanList opaqueTerms}, observesPanicPayload := false{ " }" } : Compiler.CompilationModel.SolidityImport.ImportReport)" pure (modelTerm, reportTerm, digest) private def elabDef (name : Name) (type body : String) : CommandElabM Unit := do let cmd := s!"def _root_.{name} : {type} :=\n{body}" - match Parser.runParserCategory (← getEnv) `command cmd "" with + match Parser.runParserCategory (← getEnv) `command cmd "" with | .error e => throwError e | .ok stx => elabCommand stx -syntax (name := sliceImportCmd) - "solidity_slice_import " ident - " slice_root " str " slice_entry " str " slice_contract " str " slice_function " str - " slice_param_tys " "[" str,* "]" - " slice_solc " str " slice_via_ir " ident " slice_evm " str - " slice_optimizer " ident " slice_runs " num " slice_bytecode_hash " str : command - -@[command_elab sliceImportCmd] -def elabSliceImport : CommandElab := fun stx => do +/-- `function name(T₁, …, Tₙ)`: a function to import, with its Solidity parameter types. -/ +syntax solidityRoot := &"function" ident "(" ident,* ")" + +/-- Import Solidity functions as a `CompilationModel`; see the module docstring. -/ +syntax (name := solidityImportCmd) + "solidity_import " ident &"from" str &"entry" str " using " term:max + &"contract" ident (ppLine solidityRoot)+ : command + +private unsafe def evalProfileUnsafe (stx : Term) : TermElabM Profile := do + let e ← Term.elabTermEnsuringType stx (mkConst ``Profile) + Term.synthesizeSyntheticMVarsNoPostponing + let e ← instantiateMVars e + if e.hasMVar then throwError "the solc profile is not fully determined" + Meta.evalExpr Profile (mkConst ``Profile) e + +@[implemented_by evalProfileUnsafe] +private opaque evalProfile (stx : Term) : TermElabM Profile + +/-- The directory holding the importing package's `lakefile.lean`. -/ +private def packageRoot : CommandElabM System.FilePath := do + let mut pkg := (← IO.FS.realPath (← getFileName)).parent.getD "." + while !(← (pkg / "lakefile.lean").pathExists) do + let some parent := pkg.parent | throwError "package root not found" + if parent == pkg then throwError "package root not found" + pkg := parent + return pkg + +@[command_elab solidityImportCmd] +def elabSolidityImport : CommandElab := fun stx => do + let `(solidity_import $alias from $root entry $entry using $prof contract $contract + $roots:solidityRoot*) := stx | throwUnsupportedSyntax let saved ← getEnv try if debug.skipKernelTC.get (← getOptions) then throwError "kernel checking must be enabled" - let aliasName := stx[1].getId - let rootArg := stx[3].isStrLit?.get! - let entryPath := stx[5].isStrLit?.get! - let contractName := stx[7].isStrLit?.get! - let functionName := stx[9].isStrLit?.get! - let paramTys := stx[12].getSepArgs.map fun s => s.isStrLit?.get! - let solcArg := stx[15].isStrLit?.get! - unless solcArg == solcLongVersion do - throwError "this importer is pinned to {solcLongVersion}" - let useIR := stx[17].getId == `true - let evmVersion := stx[19].isStrLit?.get! - let optimize := stx[21].getId == `true - let some runCount := stx[23].isNatLit? | throwError "optimizer runs must be a nat" - let hashMode := stx[25].isStrLit?.get! - unless useIR || stx[17].getId == `false do throwError "viaIR must be true or false" - unless optimize || stx[21].getId == `false do throwError "optimizer must be true or false" - let authored ← IO.FS.realPath (← getFileName) - let mut pkg := authored.parent.getD "." - while !(← (pkg / "lakefile.lean").pathExists) do - let some parent := pkg.parent | throwError "package root not found" - if parent == pkg then throwError "package root not found" - pkg := parent - let project := if rootArg.startsWith "/" then - System.FilePath.mk rootArg else pkg / rootArg + let profile ← liftTermElabM <| evalProfile prof + unless profile.solc == solcLongVersion do + throwError "this importer is pinned to solc {solcLongVersion}, not {profile.solc}" + let #[fnStx] := roots + | throwError "importing several functions is not supported yet" + let `(solidityRoot| function $fn ( $tys,* )) := fnStx | throwUnsupportedSyntax + let written := tys.getElems.map (·.getId.toString (escape := false)) + let pkg ← packageRoot + let project := if root.getString.startsWith "/" then + System.FilePath.mk root.getString else pkg / root.getString let (modelTerm, reportTerm, digest) ← liftTermElabM <| - importSlice pkg project entryPath contractName functionName paramTys useIR optimize evmVersion hashMode runCount - let ns := (← getCurrNamespace) ++ aliasName + importSlice pkg project entry.getString contract.getId.toString fn.getId.toString written profile + let ns := (← getCurrNamespace) ++ alias.getId elabDef (ns ++ `model) "Compiler.CompilationModel.CompilationModel" modelTerm - elabDef (ns ++ `report) "Compiler.CompilationModel.SoliditySlice.SliceReport" reportTerm + elabDef (ns ++ `report) "Compiler.CompilationModel.SolidityImport.ImportReport" reportTerm elabDef (ns ++ `sourceDigest) "String" (leanStr digest) - let covered := s!"theorem _root_.{ns ++ `sliceCovered} : Compiler.CompilationModel.SoliditySlice.modelSliceCovered {ns ++ `model} = true := by decide" - match Parser.runParserCategory (← getEnv) `command covered "" with + let covered := s!"theorem _root_.{ns ++ `covered} : Compiler.CompilationModel.SolidityImport.modelImportCovered {ns ++ `model} = true := by decide" + match Parser.runParserCategory (← getEnv) `command covered "" with | .error e => throwError e | .ok stx => elabCommand stx if (← get).messages.hasErrors then - throwError "slice import failed to check" + throwError "Solidity import failed to check" catch e => setEnv saved throw e -end Compiler.CompilationModel.SoliditySlice +end Compiler.CompilationModel.SolidityImport diff --git a/Compiler/SolidityImport/Profile.lean b/Compiler/SolidityImport/Profile.lean new file mode 100644 index 0000000000..77fe2c086f --- /dev/null +++ b/Compiler/SolidityImport/Profile.lean @@ -0,0 +1,27 @@ +/-! +Compiler settings for a Solidity import. + +A profile is an ordinary value, so it can be named, reused and `#print`ed: + +```lean +def build : Profile := + { evmVersion := "osaka", viaIR := true, optimizerRuns := some 466, bytecodeHash := "none" } +``` + +The settings are passed to solc and are part of the import digest. They should +match the settings the audited bytecode is built with. +-/ +namespace Compiler.CompilationModel.SolidityImport + +structure Profile where + /-- Exact solc release. The importer accepts only its pinned release. -/ + solc : String := "0.8.34+commit.80d5c536" + evmVersion : String + viaIR : Bool + /-- Optimizer runs; `none` disables the optimizer. -/ + optimizerRuns : Option Nat + /-- solc `metadata.bytecodeHash`, e.g. `"none"` or `"ipfs"`. -/ + bytecodeHash : String + deriving Repr, BEq, Inhabited + +end Compiler.CompilationModel.SolidityImport diff --git a/Compiler/SoliditySlice/README.md b/Compiler/SolidityImport/README.md similarity index 77% rename from Compiler/SoliditySlice/README.md rename to Compiler/SolidityImport/README.md index 0010f8afb4..d401a5240a 100644 --- a/Compiler/SoliditySlice/README.md +++ b/Compiler/SolidityImport/README.md @@ -1,29 +1,31 @@ -# Solidity function slices +# Solidity import -`solidity_slice_import` elaborates a selected Solidity function and its reached +`solidity_import` elaborates a selected Solidity function and its reached helpers into the ordinary Verity `CompilationModel`. It consumes a checksum-pinned -solc 0.8.34 AST and storage layout. There is no separate slice interpreter. +solc 0.8.34 AST and storage layout. There is no separate Solidity interpreter. ```lean -import Compiler.SoliditySlice.Import - -solidity_slice_import example - slice_root "Contracts/SoliditySliceSmoke" slice_entry "Slice.sol" - slice_contract "C" slice_function "f" - slice_param_tys ["struct Mkt", "bytes32", "address"] - slice_solc "0.8.34+commit.80d5c536" slice_via_ir true slice_evm "osaka" - slice_optimizer true slice_runs 466 slice_bytecode_hash "none" +import Compiler.SolidityImport.Import + +def build : Profile := + { evmVersion := "osaka", viaIR := true, optimizerRuns := some 466, bytecodeHash := "none" } + +solidity_import example from "Contracts/SolidityImportSmoke" entry "Slice.sol" using build + contract C + function f(Mkt, bytes32, address) ``` The command defines `example.model`, `example.report`, `example.sourceDigest`, -and the kernel theorem `example.sliceCovered`. It also works inside namespaces. +and the kernel theorem `example.covered`. It also works inside namespaces. `example.report.toText` renders the included/excluded signatures, projections, -opaque members, settings, and source digest. +opaque members, settings, and source digest. The profile can also be written +inline: `using { evmVersion := "osaka", ... }`. Parameter types use their +Solidity spelling; a struct may be qualified (`IMidnight.Market`). -Install the compiler with `python3 scripts/setup_solc_slice.py`; a downstream +Install the compiler with `python3 scripts/setup_solc_import.py`; a downstream package can pass `--output .lake/solidity-import/solc-0.8.34`. Elaboration never -downloads a compiler. `slice_root` resolves relative to the importing package's -`lakefile.lean`, and imports use the project's `remappings.txt`. +downloads a compiler. The `from` directory resolves relative to the importing +package's `lakefile.lean`, and imports use that directory's `remappings.txt`. ## Supported boundary @@ -73,8 +75,8 @@ The Morpho pilot demonstrates that workflow along with universal properties on ## Validation ```sh -lake build SoliditySliceSmoke -python3 scripts/solidity_slice_mutations.py +lake build SolidityImportSmoke +python3 scripts/solidity_import_mutations.py ``` The regression suite exercises helper/local-name hygiene, namespace use, diff --git a/Compiler/SoliditySlice/Report.lean b/Compiler/SolidityImport/Report.lean similarity index 87% rename from Compiler/SoliditySlice/Report.lean rename to Compiler/SolidityImport/Report.lean index bb44311489..b793cb59aa 100644 --- a/Compiler/SoliditySlice/Report.lean +++ b/Compiler/SolidityImport/Report.lean @@ -4,11 +4,11 @@ Report of a Solidity function slice lowered to `CompilationModel`. A slice is not a whole-contract invariant. `opaqueMembers` are layout records copied from solc and deliberately not given an executable encoding. -/ -namespace Compiler.CompilationModel.SoliditySlice +namespace Compiler.CompilationModel.SolidityImport /-- One function whose body was translated. Library helpers are included even when the executable model inlines them. -/ -structure SliceFunction where +structure ImportedFunction where contract : String name : String declId : Nat @@ -18,7 +18,7 @@ structure SliceFunction where /-- A memory-struct parameter replaced by the static members the slice reads. `headWord` is the member's index in the Solidity memory/ABI head, derived from the struct declaration order. This is not an ABI decoder. -/ -structure SliceProjection where +structure ParamProjection where parameter : String member : String structName : String @@ -38,7 +38,7 @@ structure OpaqueMember where /-- Provenance and closure report. `sourceDigest` covers sources, solc settings, the compiler release identity, and the importer sources. -/ -structure SliceReport where +structure ImportReport where importerVersion : String solcLongVersion : String solcSha256 : String @@ -46,9 +46,9 @@ structure SliceReport where sourceDigest : String contract : String rootFunction : String - includedFunctions : List SliceFunction - excludedFunctions : List SliceFunction - projections : List SliceProjection + includedFunctions : List ImportedFunction + excludedFunctions : List ImportedFunction + projections : List ParamProjection storageFields : List String opaqueMembers : List OpaqueMember /-- Proof denotation records success versus revert. `Stmt.panic` keeps its @@ -57,7 +57,7 @@ structure SliceReport where deriving Repr, BEq /-- Stable, reviewable inventory of the translated closure and explicit exclusions. -/ -def SliceReport.toText (r : SliceReport) : String := Id.run do +def ImportReport.toText (r : ImportReport) : String := Id.run do let mut lines := [s!"importer {r.importerVersion}", s!"solc {r.solcLongVersion}", s!"solcSha256 {r.solcSha256}", s!"digest {r.sourceDigest}", s!"contract {r.contract}", s!"root {r.rootFunction}", s!"settings {r.settingsJson}"] @@ -73,4 +73,4 @@ def SliceReport.toText (r : SliceReport) : String := Id.run do lines := lines ++ [s!"opaque {m.field}.{m.name} {m.solcType} word {m.wordOffset} byte {m.byteOffset}"] return String.intercalate "\n" lines ++ "\n" -end Compiler.CompilationModel.SoliditySlice +end Compiler.CompilationModel.SolidityImport diff --git a/Compiler/SoliditySlice/TESTING.md b/Compiler/SolidityImport/TESTING.md similarity index 99% rename from Compiler/SoliditySlice/TESTING.md rename to Compiler/SolidityImport/TESTING.md index e4bd44cce1..b200286de7 100644 --- a/Compiler/SoliditySlice/TESTING.md +++ b/Compiler/SolidityImport/TESTING.md @@ -40,7 +40,7 @@ Run from a Lake workspace (Verity or a downstream project): ```sh scripts/check_solidity_differential.sh \ - --config Contracts/SoliditySliceSmoke/differential.json \ + --config Contracts/SolidityImportSmoke/differential.json \ --output .lake/differential/smoke --seed 2438 --cases 128 scripts/check_solidity_differential.sh \ diff --git a/Contracts/SoliditySliceSmoke/Lib.sol b/Contracts/SolidityImportSmoke/Lib.sol similarity index 100% rename from Contracts/SoliditySliceSmoke/Lib.sol rename to Contracts/SolidityImportSmoke/Lib.sol diff --git a/Contracts/SoliditySliceSmoke/Slice.sol b/Contracts/SolidityImportSmoke/Slice.sol similarity index 100% rename from Contracts/SoliditySliceSmoke/Slice.sol rename to Contracts/SolidityImportSmoke/Slice.sol diff --git a/Contracts/SoliditySliceSmoke/Smoke.lean b/Contracts/SolidityImportSmoke/Smoke.lean similarity index 83% rename from Contracts/SoliditySliceSmoke/Smoke.lean rename to Contracts/SolidityImportSmoke/Smoke.lean index 04ca6e58c3..dc40b283e9 100644 --- a/Contracts/SoliditySliceSmoke/Smoke.lean +++ b/Contracts/SolidityImportSmoke/Smoke.lean @@ -1,16 +1,16 @@ -import Compiler.SoliditySlice.Import -import Compiler.SoliditySlice.Coverage +import Compiler.SolidityImport.Import +import Compiler.SolidityImport.Coverage open Compiler.CompilationModel -open Compiler.CompilationModel.SoliditySlice +open Compiler.CompilationModel.SolidityImport open Compiler.CompilationModel.Denote -solidity_slice_import slice - slice_root "Contracts/SoliditySliceSmoke" slice_entry "Slice.sol" - slice_contract "C" slice_function "f" - slice_param_tys ["struct Mkt", "bytes32", "address"] - slice_solc "0.8.34+commit.80d5c536" slice_via_ir true slice_evm "osaka" - slice_optimizer true slice_runs 466 slice_bytecode_hash "none" +def smokeBuild : Profile := + { evmVersion := "osaka", viaIR := true, optimizerRuns := some 466, bytecodeHash := "none" } + +solidity_import smoke from "Contracts/SolidityImportSmoke" entry "Slice.sol" using smokeBuild + contract C + function f(Mkt, bytes32, address) /-- Oracle used only to place the two struct words of this witness. The numeric claim is about the values `structMember` reads back. -/ @@ -36,7 +36,7 @@ def witnessBindings (maturity : Nat) : Env := [("m_maturity", maturity), ("id", 1), ("user", 2)] private def smokeBody : List Stmt := - match slice.model.functions with + match smoke.model.functions with | fn :: _ => fn.body | [] => [] @@ -60,11 +60,11 @@ theorem string_beq_kernel : ("m_maturity" == "id") = false := by set_option maxHeartbeats 2000000 in theorem smoke_step_credit : - execStmt sliceOracle slice.model.fields (witnessState 100 10 0 0 0 50 100) (peel smokeBody).1 = + execStmt sliceOracle smoke.model.fields (witnessState 100 10 0 0 0 50 100) (peel smokeBody).1 = .continue { witnessState 100 10 0 0 0 50 100 with bindings := bindValue (witnessBindings 100) "_verity_slice_tmp_0" 100 } := by - dsimp only [peel, smokeBody, slice.model] + dsimp only [peel, smokeBody, smoke.model] have hmi : ("m_maturity" == "id") = false := by decide have hmu : ("m_maturity" == "user") = false := by decide have hiu : ("id" == "user") = false := by decide @@ -86,5 +86,5 @@ theorem smoke_step_credit : Verity.Core.Uint256.modulus = 100 := by decide simp [hcredit] --- Interpreter witnesses A/B live in scripts/solidity_slice_mutations.py. +-- Interpreter witnesses A/B live in scripts/solidity_import_mutations.py. -- Keeping test execution out of this proof module satisfies Lean hygiene. diff --git a/Contracts/SoliditySliceSmoke/differential-cases.json b/Contracts/SolidityImportSmoke/differential-cases.json similarity index 100% rename from Contracts/SoliditySliceSmoke/differential-cases.json rename to Contracts/SolidityImportSmoke/differential-cases.json diff --git a/Contracts/SoliditySliceSmoke/differential.json b/Contracts/SolidityImportSmoke/differential.json similarity index 100% rename from Contracts/SoliditySliceSmoke/differential.json rename to Contracts/SolidityImportSmoke/differential.json diff --git a/Contracts/SoliditySliceSmoke/model.golden b/Contracts/SolidityImportSmoke/model.golden similarity index 100% rename from Contracts/SoliditySliceSmoke/model.golden rename to Contracts/SolidityImportSmoke/model.golden diff --git a/TRUST_ASSUMPTIONS.md b/TRUST_ASSUMPTIONS.md index 44a5969c63..ca31cac32f 100644 --- a/TRUST_ASSUMPTIONS.md +++ b/TRUST_ASSUMPTIONS.md @@ -4,11 +4,11 @@ This document states what Verity proves and what it still trusts. ## Solidity function-slice import -`Compiler/SoliditySlice/Import.lean` selects one function from a solc project, +`Compiler/SolidityImport/Import.lean` selects one function from a solc project, closes over the definitions solc's declaration ids actually reach, and elaborates a `CompilationModel`. Execution of an accepted slice is `Compiler.CompilationModel.Denote.execStmt`, restricted by -`stmtListCovered` in `Compiler/SoliditySlice/Coverage.lean`. That predicate is +`stmtListCovered` in `Compiler/SolidityImport/Coverage.lean`. That predicate is not `SupportedFunction`: the slice uses `panic`, `returnValues`, packed `structMember` / `structMember2`, and checked arithmetic, which the IR raccord does not cover. `DenoteAgreement.execStmt_eq` still holds, including diff --git a/artifacts/trust_surface_report.json b/artifacts/trust_surface_report.json index 589e86c834..aeec1da665 100644 --- a/artifacts/trust_surface_report.json +++ b/artifacts/trust_surface_report.json @@ -167,7 +167,7 @@ ] }, "mechanisms": { - "@[implemented_by": 1, + "@[implemented_by": 2, "native_decide": 613, "partial def": 193 }, diff --git a/lakefile.lean b/lakefile.lean index 4108b72854..523012a58e 100644 --- a/lakefile.lean +++ b/lakefile.lean @@ -51,8 +51,8 @@ lean_lib «Contracts» where .andSubmodules `Contracts.ReentrancyRelyGuarantee ] -lean_lib «SoliditySliceSmoke» where - globs := #[.one `Contracts.SoliditySliceSmoke.Smoke] +lean_lib «SolidityImportSmoke» where + globs := #[.one `Contracts.SolidityImportSmoke.Smoke] lean_lib «Compiler» where globs := #[.andSubmodules `Compiler] diff --git a/scripts/check_solidity_differential.sh b/scripts/check_solidity_differential.sh index 62b6a03bc5..5223c551bd 100755 --- a/scripts/check_solidity_differential.sh +++ b/scripts/check_solidity_differential.sh @@ -7,5 +7,5 @@ if [ ! -x "$verity_test_venv/bin/python" ]; then python3 -m venv "$verity_test_venv" fi "$verity_test_venv/bin/python" -m pip install --disable-pip-version-check -q -r "$verity_test_root/scripts/solidity_differential/requirements.txt" -python3 "$verity_test_root/scripts/setup_solc_slice.py" --output .lake/solidity-import/solc-0.8.34 -exec "$verity_test_venv/bin/python" "$verity_test_root/scripts/solidity_slice_differential.py" "$@" +python3 "$verity_test_root/scripts/setup_solc_import.py" --output .lake/solidity-import/solc-0.8.34 +exec "$verity_test_venv/bin/python" "$verity_test_root/scripts/solidity_import_differential.py" "$@" diff --git a/scripts/setup_solc_slice.py b/scripts/setup_solc_import.py similarity index 100% rename from scripts/setup_solc_slice.py rename to scripts/setup_solc_import.py diff --git a/scripts/solidity_differential/engine.py b/scripts/solidity_differential/engine.py index 221be8eabb..ba2779200d 100644 --- a/scripts/solidity_differential/engine.py +++ b/scripts/solidity_differential/engine.py @@ -49,7 +49,7 @@ def implementation_hashes(): for directory in ("Compiler", "Verity", "scripts/solidity_differential") for p in (ROOT / directory).rglob("*") if p.suffix in (".lean", ".py", ".sol", ".txt")} - for name in ("scripts/solidity_slice_differential.py", "lake-manifest.json", "lean-toolchain"): + for name in ("scripts/solidity_import_differential.py", "lake-manifest.json", "lean-toolchain"): hashes[name] = hashlib.sha256((ROOT / name).read_bytes()).hexdigest() return hashes @@ -73,19 +73,22 @@ def prepare(config_path, output): "metadata": {"bytecodeHash": "none"}, **settings} if profile["evmVersion"] != "osaka": raise HarnessError("fixture_unsupported: this runner currently targets Osaka") - command(["lake", "build", "Compiler.SoliditySlice.Import", "Compiler.SoliditySlice.Differential"], + command(["lake", "build", "Compiler.SolidityImport.Import", "Compiler.SolidityImport.Differential"], timeout=1800, log=output / "build.log") # Keep the importer and reference compiler profile identical. driver = output / "Driver.lean" - driver.write_text('import Compiler.SoliditySlice.Import\nimport Compiler.SoliditySlice.Differential\n' - + 'solidity_slice_import tested\n' - + f' slice_root {json.dumps(str(project))} slice_entry {json.dumps(config["entry"])}\n' - + f' slice_contract {json.dumps(config["contract"])} slice_function {json.dumps(config["function"])}\n' - + ' slice_param_tys [' + ', '.join(map(json.dumps, config["param_types"])) + ']\n' - + f' slice_solc "0.8.34+commit.80d5c536" slice_via_ir {str(profile["viaIR"]).lower()} slice_evm {json.dumps(profile["evmVersion"])}\n' - + f' slice_optimizer {str(profile["optimizer"]["enabled"]).lower()} slice_runs {profile["optimizer"]["runs"]} slice_bytecode_hash {json.dumps(profile["metadata"]["bytecodeHash"])}\n' + runs = f'some {profile["optimizer"]["runs"]}' if profile["optimizer"]["enabled"] else "none" + # solc type strings ("struct Market") become Solidity spellings ("Market"). + written = [ty.split(" ", 1)[1] if ty.split(" ", 1)[0] in ("struct", "contract", "enum") else ty + for ty in config["param_types"]] + driver.write_text('import Compiler.SolidityImport.Import\nimport Compiler.SolidityImport.Differential\n' + + f'solidity_import tested from {json.dumps(str(project))} entry {json.dumps(config["entry"])}\n' + + f' using {{ evmVersion := {json.dumps(profile["evmVersion"])}, viaIR := {str(profile["viaIR"]).lower()}, ' + + f'optimizerRuns := {runs}, bytecodeHash := {json.dumps(profile["metadata"]["bytecodeHash"])} }}\n' + + f' contract {config["contract"]}\n' + + f' function {config["function"]}({", ".join(written)})\n' + 'def main (args : List String) : IO UInt32 :=\n' - + ' Compiler.CompilationModel.SoliditySlice.Differential.run tested.model tested.report args\n') + + ' Compiler.CompilationModel.SolidityImport.Differential.run tested.model tested.report args\n') command(["lake", "env", "lean", "--run", driver, "describe", output / "model.json", output / "model.yul"], timeout=300, log=output / "import.log") metadata = json.loads((output / "model.json").read_text()) diff --git a/scripts/solidity_differential/mutations.py b/scripts/solidity_differential/mutations.py index 27a5575880..37b6bf6e1a 100644 --- a/scripts/solidity_differential/mutations.py +++ b/scripts/solidity_differential/mutations.py @@ -8,8 +8,8 @@ from .engine import ROOT, WORKSPACE, HarnessError, command, write_json MUTANTS = { - "import-comparison": ("Compiler/SoliditySlice/Import.lean", '| "<" => cmp "lt" left right', '| "<" => cmp "gt" left right'), - "import-field-slot": ("Compiler/SoliditySlice/Import.lean", 'slot := some {slot}', 'slot := some {slot + 1}'), + "import-comparison": ("Compiler/SolidityImport/Import.lean", '| "<" => cmp "lt" left right', '| "<" => cmp "gt" left right'), + "import-field-slot": ("Compiler/SolidityImport/Import.lean", 'slot := some {slot}', 'slot := some {slot + 1}'), "denote-packed-mask": ("Verity/Core/Model/Denote.lean", '(2 ^ packed.width) - 1', '(2 ^ packed.width) - 2'), "denote-storage-read": ("Verity/Core/Model/Denote.lean", ' world.readSlot (wordNormalize slot)\n', ' world.readSlot (wordNormalize (slot + 1))\n'), } @@ -51,8 +51,8 @@ def mutation_campaign(output, selected=None): if text.count(before) != 1: raise HarnessError("mutation anchor drifted: " + name) source.write_text(text.replace(before, after)) - argv = [sys.executable, str(directory / "scripts/solidity_slice_differential.py"), - "--config", str(directory / "Contracts/SoliditySliceSmoke/differential.json"), + argv = [sys.executable, str(directory / "scripts/solidity_import_differential.py"), + "--config", str(directory / "Contracts/SolidityImportSmoke/differential.json"), "--output", str(directory / ".lake/campaign"), "--cases", "8"] try: result = subprocess.run(argv, cwd=directory, text=True, capture_output=True, timeout=600) diff --git a/scripts/solidity_slice_differential.py b/scripts/solidity_import_differential.py similarity index 100% rename from scripts/solidity_slice_differential.py rename to scripts/solidity_import_differential.py diff --git a/scripts/solidity_slice_mutations.py b/scripts/solidity_import_mutations.py similarity index 92% rename from scripts/solidity_slice_mutations.py rename to scripts/solidity_import_mutations.py index c4ce78fd5e..92ecab95db 100644 --- a/scripts/solidity_slice_mutations.py +++ b/scripts/solidity_import_mutations.py @@ -1,8 +1,8 @@ #!/usr/bin/env python3 -"""Mutation checks for the Solidity slice importer. +"""Mutation checks for the Solidity importer. Each case copies the smoke project, changes one thing, and elaborates -`solidity_slice_import`. A reached helper, a return-order change, and a layout +`solidity_import`. A reached helper, a return-order change, and a layout change must make the witness check fail. An unreached `for` stays importable. Reaching that `for` must be rejected with a source position, and the failed process must not leave an olean. @@ -15,24 +15,22 @@ ROOT = Path(__file__).resolve().parents[1] SOLC = ROOT / ".lake/solidity-import/solc-0.8.34" -WORK = ROOT / ".lake/slice-mutations" -SMOKE = ROOT / "Contracts/SoliditySliceSmoke" +WORK = ROOT / ".lake/import-mutations" +SMOKE = ROOT / "Contracts/SolidityImportSmoke" GOLDEN = SMOKE / "model.golden" HEADER = """ -import Compiler.SoliditySlice.Import -import Compiler.SoliditySlice.Coverage +import Compiler.SolidityImport.Import +import Compiler.SolidityImport.Coverage open Compiler.CompilationModel -open Compiler.CompilationModel.SoliditySlice +open Compiler.CompilationModel.SolidityImport open Compiler.CompilationModel.Denote -solidity_slice_import imported - slice_root "{root}" slice_entry "Slice.sol" - slice_contract "C" slice_function "f" - slice_param_tys ["struct Mkt", "bytes32", "address"] - slice_solc "0.8.34+commit.80d5c536" slice_via_ir true slice_evm "osaka" - slice_optimizer true slice_runs 466 slice_bytecode_hash "none" +solidity_import imported from "{root}" entry "Slice.sol" + using {{ evmVersion := "osaka", viaIR := true, optimizerRuns := some 466, bytecodeHash := "none" }} + contract C + function f(Mkt, bytes32, address) """ WITNESS = r""" @@ -277,8 +275,8 @@ def main() -> None: expect_failure("projection-name-collision", write_project("projection-name", projected_name_collision), "projected parameter name collision") namespaced = WORK / "base/Namespaced.lean" namespaced.write_text(HEADER.format(root=WORK / "base").replace( - "solidity_slice_import imported", "namespace Nested\nsolidity_slice_import imported") + - "\nend Nested\nexample : modelSliceCovered Nested.imported.model = true := Nested.imported.sliceCovered\n") + "solidity_import imported", "namespace Nested\nsolidity_import imported") + + "\nend Nested\nexample : modelImportCovered Nested.imported.model = true := Nested.imported.covered\n") result = lean(namespaced) if result.returncode: raise SystemExit(result.stdout + result.stderr) @@ -303,12 +301,12 @@ def main() -> None: expect_failure("return-order", write_project("returns", swap_returns), "witness changed") expect_failure("layout", write_project("layout", swap_layout), "witness changed") diagnostic = expect_failure("reachable-for", write_project("reachable", call_unused), "unsupported") - assert "[solidity-slice:unsupported]" in diagnostic and "closure:" in diagnostic + assert "[solidity-import:unsupported]" in diagnostic and "closure:" in diagnostic assert "Lib.sol:" in diagnostic and "closure: C.f -> L.unused" in diagnostic, diagnostic if digest_of(base) == digest_of(rounding): raise SystemExit("helper change did not move the digest") print("pass helper-digest") - print("slice mutations passed") + print("import mutations passed") if __name__ == "__main__": diff --git a/scripts/verify_sync_spec.json b/scripts/verify_sync_spec.json index 22e65614af..61994b25a7 100644 --- a/scripts/verify_sync_spec.json +++ b/scripts/verify_sync_spec.json @@ -24,10 +24,10 @@ "lakefile.lean", "lake-manifest.json", "scripts/solidity_differential/**", - "scripts/solidity_slice_differential.py", + "scripts/solidity_import_differential.py", "scripts/check_solidity_differential.sh", - "scripts/solidity_slice_mutations.py", - "scripts/setup_solc_slice.py", + "scripts/solidity_import_mutations.py", + "scripts/setup_solc_import.py", "lean-toolchain" ], "compiler_paths": [ @@ -54,10 +54,10 @@ "lakefile.lean", "lake-manifest.json", "scripts/solidity_differential/**", - "scripts/solidity_slice_differential.py", + "scripts/solidity_import_differential.py", "scripts/check_solidity_differential.sh", - "scripts/solidity_slice_mutations.py", - "scripts/setup_solc_slice.py", + "scripts/solidity_import_mutations.py", + "scripts/setup_solc_import.py", "lean-toolchain", "foundry.toml" ], @@ -834,12 +834,12 @@ "expected_checks_other_commands": [], "expected_build_commands": [ "check_lean_warning_regression.py --log lake-build.log", - "setup_solc_slice.py", - "solidity_slice_mutations.py" + "setup_solc_import.py", + "solidity_import_mutations.py" ], "required_build_run_commands": [ "lake build PrintAxioms", - "lake build SoliditySliceSmoke" + "lake build SolidityImportSmoke" ], "expected_build_audit_commands": [ "check_split_package_builds.py", @@ -920,7 +920,7 @@ "static-gas-report-patched" ], "compiler-regressions": [ - "solidity-slice-differential" + "solidity-import-differential" ], "lean-profile": [ "lean-perf-queue" @@ -946,7 +946,7 @@ "gas-report-static-patched.tsv" ], "compiler-regressions": [ - ".lake/slice-differential/**\n!.lake/slice-differential/**/.lake/build/**\n!.lake/slice-differential/**/.lake/packages/**" + ".lake/import-differential/**\n!.lake/import-differential/**/.lake/build/**\n!.lake/import-differential/**/.lake/packages/**" ], "lean-profile": [ "lean-perf-queue.md" diff --git a/scripts/verify_sync_spec_source.py b/scripts/verify_sync_spec_source.py index 6e1a34ae5d..de650bc0fc 100644 --- a/scripts/verify_sync_spec_source.py +++ b/scripts/verify_sync_spec_source.py @@ -706,9 +706,9 @@ "python3 -m unittest discover -s scripts -p 'test_*.py' -v"], 'expected_checks_other_commands': [], 'expected_build_commands': ['check_lean_warning_regression.py --log lake-build.log', - 'setup_solc_slice.py', - 'solidity_slice_mutations.py'], - 'required_build_run_commands': ['lake build PrintAxioms', 'lake build SoliditySliceSmoke'], + 'setup_solc_import.py', + 'solidity_import_mutations.py'], + 'required_build_run_commands': ['lake build PrintAxioms', 'lake build SolidityImportSmoke'], 'expected_build_audit_commands': ['check_split_package_builds.py', 'check_axioms.py', 'check_proof_length.py --format=markdown >> ' @@ -934,9 +934,9 @@ def build_spec() -> dict: # Solidity slice differential changes require the compiled-model validation lane. for _lane in ("build_paths", "compiler_paths"): - SPEC[_lane][SPEC[_lane].index("lean-toolchain"):SPEC[_lane].index("lean-toolchain")] = ['scripts/solidity_differential/**', 'scripts/solidity_slice_differential.py', 'scripts/check_solidity_differential.sh', 'scripts/solidity_slice_mutations.py', 'scripts/setup_solc_slice.py'] -SPEC["expected_uploaded_artifacts"]["compiler-regressions"] = ["solidity-slice-differential"] -SPEC["expected_uploaded_artifact_paths"]["compiler-regressions"] = [".lake/slice-differential/**\n!.lake/slice-differential/**/.lake/build/**\n!.lake/slice-differential/**/.lake/packages/**"] + SPEC[_lane][SPEC[_lane].index("lean-toolchain"):SPEC[_lane].index("lean-toolchain")] = ['scripts/solidity_differential/**', 'scripts/solidity_import_differential.py', 'scripts/check_solidity_differential.sh', 'scripts/solidity_import_mutations.py', 'scripts/setup_solc_import.py'] +SPEC["expected_uploaded_artifacts"]["compiler-regressions"] = ["solidity-import-differential"] +SPEC["expected_uploaded_artifact_paths"]["compiler-regressions"] = [".lake/import-differential/**\n!.lake/import-differential/**/.lake/build/**\n!.lake/import-differential/**/.lake/packages/**"] for _key in ("expected_uploaded_artifacts", "expected_uploaded_artifact_paths"): _profile = SPEC[_key].pop("lean-profile") SPEC[_key]["lean-profile"] = _profile From e92b99543de6adfdaf68df0b186497027ec9616c Mon Sep 17 00:00:00 2001 From: Thomas Marchand Date: Thu, 24 Sep 2026 15:46:35 +0200 Subject: [PATCH 13/22] Build imported models as values instead of generated Lean source The importer lowered Solidity into Lean source strings and re-parsed them with runParserCategory. Lowering now produces ordinary CompilationModel values (Expr, Stmt, Field, FunctionSpec) and Quote.lean turns the emitted fragment into syntax quotations; anything outside that fragment is an internal error. After elaboration, each import evaluates `x.model` and `x.report` back and requires them to equal the imported values. The smoke library declares its Solidity sources as a Lake `input_dir`, so editing a .sol file rebuilds the import. The imported smoke model is unchanged (model.golden), and all four A/B/C mutants are still detected. --- Compiler/SolidityImport/Import.lean | 371 ++++++++++----------- Compiler/SolidityImport/Quote.lean | 144 ++++++++ Compiler/SolidityImport/README.md | 18 +- TRUST_ASSUMPTIONS.md | 5 +- artifacts/trust_surface_report.json | 4 +- lakefile.lean | 7 + scripts/solidity_differential/mutations.py | 4 +- 7 files changed, 348 insertions(+), 205 deletions(-) create mode 100644 Compiler/SolidityImport/Quote.lean diff --git a/Compiler/SolidityImport/Import.lean b/Compiler/SolidityImport/Import.lean index d9e6cabe78..f22b26c27a 100644 --- a/Compiler/SolidityImport/Import.lean +++ b/Compiler/SolidityImport/Import.lean @@ -2,6 +2,7 @@ import Lean import Compiler.Sha256.Engine import Compiler.SolidityImport.Coverage import Compiler.SolidityImport.Profile +import Compiler.SolidityImport.Quote import Compiler.SolidityImport.Report /-! @@ -93,34 +94,21 @@ private def sha256Hex (bytes : ByteArray) : String := (Sha256Engine.sha256 bytes).data.foldl (init := "") fun acc byte => acc.push (hexDigit (byte.toNat / 16)) |>.push (hexDigit (byte.toNat % 16)) -private def leanStr (s : String) : String := - let escaped := s.foldl (init := "") fun acc c => - if c == '\"' || c == '\\' then acc ++ "\\" ++ c.toString else acc.push c - "\"" ++ escaped ++ "\"" - -private def leanList (xs : Array String) : String := - "[" ++ String.intercalate ", " xs.toList ++ "]" - -private def ex (body : String) : String := - s!"(Compiler.CompilationModel.Expr.{body})" - -private def st (body : String) : String := - s!"(Compiler.CompilationModel.Stmt.{body})" - +/-- A lowered expression and the statements that must run before it. -/ private structure Val where - pre : Array String - expr : String + pre : Array Stmt + expr : Expr private inductive SPath where - | one (field key : String) - | two (field k1 k2 : String) - | outer (field k1 : String) + | one (field : String) (key : Expr) + | two (field : String) (k1 k2 : Expr) + | outer (field : String) (k1 : Expr) private inductive Ref where | expr (v : Val) - | path (pre : Array String) (p : SPath) - | mem (id : Nat) (pre : Array String) - | state (name : String) (pre : Array String) + | path (pre : Array Stmt) (p : SPath) + | mem (id : Nat) (pre : Array Stmt) + | state (name : String) (pre : Array Stmt) private structure MemParam where param : String @@ -129,7 +117,7 @@ private structure MemParam where private structure FieldInfo where slot : Nat - term : String + field : Field keyCount : Nat memberNames : Array String opaqueNames : Array String @@ -166,7 +154,7 @@ private structure Env where funContract : RBMap Nat String compare structs : RBMap Nat Json compare stateVars : RBMap Nat Json compare - values : RBMap Nat String compare + values : RBMap Nat Expr compare paths : RBMap Nat SPath compare mems : RBMap Nat MemParam compare fieldsByName : RBMap String FieldInfo compare @@ -176,10 +164,10 @@ private structure Env where projections : Array ProjRec opaqueMembers : Array OpaqRec referenced : Array String - scalarTy : RBMap Nat String compare + scalarTy : RBMap Nat ParamType compare next : Nat stack : List Nat - yulNames : RBMap String String compare + yulNames : RBMap String Expr compare currentFile : String private def Env.init : Env where @@ -266,19 +254,16 @@ private def fresh : M String := do unless reserved.contains candidate do return candidate throwError "unable to allocate a hygienic slice binding" -private def isAtom (expr : String) : Bool := - expr.startsWith "(Compiler.CompilationModel.Expr.literal " || - expr.startsWith "(Compiler.CompilationModel.Expr.localVar " || - expr.startsWith "(Compiler.CompilationModel.Expr.param " || - expr == "(Compiler.CompilationModel.Expr.blockTimestamp)" +private def isAtom : Expr → Bool + | .literal _ | .localVar _ | .param _ | .blockTimestamp => true + | _ => false private def atom (v : Val) : M Val := do if v.pre.isEmpty && isAtom v.expr then pure v else let n ← fresh - pure { pre := v.pre.push (st s!"letVar {leanStr n} {v.expr}"), - expr := ex s!"localVar {leanStr n}" } + pure { pre := v.pre.push (.letVar n v.expr), expr := .localVar n } private def bitsOf (ty : String) : Option Nat := if ty == "uint256" || ty == "uint" then some 256 @@ -287,18 +272,15 @@ private def bitsOf (ty : String) : Option Nat := else if ty.startsWith "uint" then (ty.drop 4).toNat? else none -private def paramTypeSyntax (ty : String) : Option String := +private def paramType (ty : String) : Option ParamType := match ty with - | "uint256" | "uint" => some "Compiler.CompilationModel.ParamType.uint256" - | "address" => some "Compiler.CompilationModel.ParamType.address" - | "bytes32" => some "Compiler.CompilationModel.ParamType.bytes32" - | "bool" => some "Compiler.CompilationModel.ParamType.bool" + | "uint256" | "uint" => some .uint256 + | "address" => some .address + | "bytes32" => some .bytes32 + | "bool" => some .bool | _ => match bitsOf ty with - | some n => - if n != 256 && ty.startsWith "uint" then - some s!"(Compiler.CompilationModel.ParamType.uintN {n})" - else none + | some n => if n != 256 && ty.startsWith "uint" then some (.uintN n) else none | none => none private def noteFn (fn : Json) : M Unit := do @@ -325,11 +307,11 @@ private def noteProjection (p : ProjRec) : M Unit := do unless env.projections.any (fun q => q.parameter == p.parameter && q.member == p.member) do modify fun e => { e with projections := e.projections.push p } -private def keySyntax (ty : String) : MetaM String := +private def mappingKey (ty : String) : MetaM MappingKeyType := match ty with - | "t_bytes32" => pure "Compiler.CompilationModel.MappingKeyType.bytes32" - | "t_address" => pure "Compiler.CompilationModel.MappingKeyType.address" - | "t_uint256" => pure "Compiler.CompilationModel.MappingKeyType.uint256" + | "t_bytes32" => pure .bytes32 + | "t_address" => pure .address + | "t_uint256" => pure .uint256 | _ => throwError "unsupported mapping key {ty}" private def layoutType (types : Json) (id : String) : MetaM Json := @@ -342,13 +324,13 @@ private def buildField (types item : Json) : M FieldInfo := do let top ← liftM (layoutType types typeId) unless (← mStr (← mField top "encoding")) == "mapping" do throwError "storage field {name} is not a mapping" - let key1 ← liftM (keySyntax (← mStr (← mField top "key"))) + let key1 ← liftM (mappingKey (← mStr (← mField top "key"))) let valueId ← mStr (← mField top "value") let value ← liftM (layoutType types valueId) let encoding ← mStr (← mField value "encoding") let (key2, structTy) ← if encoding == "mapping" then - let key2 ← liftM (keySyntax (← mStr (← mField value "key"))) + let key2 ← liftM (mappingKey (← mStr (← mField value "key"))) let innerId ← mStr (← mField value "value") pure (some key2, ← liftM (layoutType types innerId)) else if encoding == "inplace" then @@ -356,7 +338,7 @@ private def buildField (types item : Json) : M FieldInfo := do else throwError "unsupported mapping value encoding {encoding} for {name}" let members ← mArr (← mField structTy "members") - let mut terms : Array String := #[] + let mut members' : Array StructMember := #[] let mut names : Array String := #[] let mut skipped : Array String := #[] for m in members do @@ -373,27 +355,19 @@ private def buildField (types item : Json) : M FieldInfo := do let some bits := (solcType.drop 6).toNat? | throwError "bad uint {solcType}" let bitOff := byteOff * 8 unless bitOff + bits ≤ 256 do throwError "packed field {label} does not fit in a word" - let packed := - if bits == 256 && bitOff == 0 then "none" - else - s!"some ({ "{ " }offset := {bitOff}, width := {bits}{ " }" } : Compiler.CompilationModel.PackedBits)" - let sty := - if bits == 16 then "Compiler.CompilationModel.StructMemberType.uint16" - else "Compiler.CompilationModel.StructMemberType.uint256" - terms := terms.push - s!"({ "{ " }name := {leanStr label}, ty := {sty}, wordOffset := {word}, packed := {packed}{ " }" } : Compiler.CompilationModel.StructMember)" + let packed : Option PackedBits := + if bits == 256 && bitOff == 0 then none else some { offset := bitOff, width := bits } + let ty : StructMemberType := if bits == 16 then .uint16 else .uint256 + members' := members'.push { name := label, ty, wordOffset := word, packed } names := names.push label else throwError "unsupported layout member {label} : {solcType}" - let tyExpr := + let ty : FieldType := match key2 with - | some k2 => - s!"Compiler.CompilationModel.FieldType.mappingStruct2 {key1} {k2} {leanList terms}" - | none => - s!"Compiler.CompilationModel.FieldType.mappingStruct {key1} {leanList terms}" - let term := - s!"({ "{ " }name := {leanStr name}, ty := {tyExpr}, slot := some {slot}{ " }" } : Compiler.CompilationModel.Field)" - pure { slot, term, keyCount := if key2.isSome then 2 else 1, + | some k2 => .mappingStruct2 key1 k2 members'.toList + | none => .mappingStruct key1 members'.toList + let field : Field := { name, ty, slot := some slot } + pure { slot, field, keyCount := if key2.isSome then 2 else 1, memberNames := names, opaqueNames := skipped } /-- Resolve only reached fields: an unrelated unsupported layout must not @@ -407,10 +381,10 @@ private def resolveField (name : String) (at_ : Json) : M FieldInfo := do modify fun e => { e with fieldsByName := e.fieldsByName.insert name info } return info -private def memberRead (pre : Array String) (path : SPath) (member : String) (at_ : Json) : M Val := do - let (fieldName, exprBody) ← match path with - | .one field key => pure (field, s!"structMember {leanStr field} {key} {leanStr member}") - | .two field k1 k2 => pure (field, s!"structMember2 {leanStr field} {k1} {k2} {leanStr member}") +private def memberRead (pre : Array Stmt) (path : SPath) (member : String) (at_ : Json) : M Val := do + let (fieldName, read) ← match path with + | .one field key => pure (field, Expr.structMember field key member) + | .two field k1 k2 => pure (field, Expr.structMember2 field k1 k2 member) | .outer _ _ => failAt at_ "member access on an incomplete mapping" let some info := (← get).fieldsByName.find? fieldName | failAt at_ s!"no storage layout for {fieldName}" @@ -419,35 +393,19 @@ private def memberRead (pre : Array String) (path : SPath) (member : String) (at unless info.memberNames.contains member do failAt at_ s!"member {member} is not a layout member of {fieldName}" markField fieldName - pure { pre, expr := ex exprBody } - -private def bin (op lhs rhs : String) : String := - ex (op ++ " " ++ lhs ++ " " ++ rhs) - -private def lit (n : Nat) : String := - ex s!"literal {n}" - -private def localName (name : String) : String := - ex ("localVar " ++ leanStr name) + pure { pre, expr := read } -private def letBind (name value : String) : String := - st ("letVar " ++ leanStr name ++ " " ++ value) +/-- `if cond then [yes] else [no]` with single-statement branches. -/ +private def iteStmt (cond : Expr) (yes no : Stmt) : Stmt := + .ite cond [yes] [no] -private def assignBind (name value : String) : String := - st ("assignVar " ++ leanStr name ++ " " ++ value) +private def overflowPanic : Stmt := .panic .arithmeticOverflow -private def iteStmt (cond thenStmt elseStmt : String) : String := - st ("ite " ++ cond ++ " [" ++ thenStmt ++ "] [" ++ elseStmt ++ "]") - -private def overflowPanic : String := - st "panic Verity.Core.PanicCode.arithmeticOverflow" - -private def divPanic : String := - st "panic Verity.Core.PanicCode.divisionByZero" +private def divPanic : Stmt := .panic .divisionByZero mutual -private partial def lowerYul (j : Json) : M String := do +private partial def lowerYul (j : Json) : M Expr := do match ← mKind j with | "YulIdentifier" => let name ← mStr (← mField j "name") @@ -457,13 +415,13 @@ private partial def lowerYul (j : Json) : M String := do | "YulFunctionCall" => let fname ← mStr (← mField (← mField j "functionName") "name") let args ← mArr (← mField j "arguments") - let mut xs : Array String := #[] + let mut xs : Array Expr := #[] for arg in args do xs := xs.push (← lowerYul arg) match fname, xs with - | "xor", #[a, b] => pure (ex s!"bitXor {a} {b}") - | "mul", #[a, b] => pure (ex s!"mul {a} {b}") - | "lt", #[a, b] => pure (ex s!"lt {a} {b}") + | "xor", #[a, b] => pure (.bitXor a b) + | "mul", #[a, b] => pure (.mul a b) + | "lt", #[a, b] => pure (.lt a b) | _, _ => failAt j s!"unsupported Yul builtin {fname}" | kind => failAt j s!"unsupported Yul node {kind}" @@ -472,7 +430,7 @@ private partial def lowerExpr (j : Json) : M Val := do | "Literal" => let raw := optStr j "value" |>.getD "" let some n := raw.toNat? | failAt j s!"unsupported literal {raw}" - pure { pre := #[], expr := ex s!"literal {n}" } + pure { pre := #[], expr := .literal n } | "TupleExpression" => if ← mBool (← mField j "isInlineArray") then failAt j "inline arrays are outside this slice" let cs ← mArr (← mField j "components") @@ -531,7 +489,7 @@ private partial def lowerRef (j : Json) : M Ref := do if member == "timestamp" && (← mKind base) == "Identifier" && optStr base "name" == some "block" && optStr ((field? base "typeDescriptions").getD Json.null) "typeIdentifier" == some "t_magic_block" then - pure (.expr { pre := #[], expr := ex "blockTimestamp" }) + pure (.expr { pre := #[], expr := .blockTimestamp }) else if member == "max" then lowerTypeMax j base else @@ -556,9 +514,9 @@ private partial def lowerTypeMax (at_ base : Json) : M Ref := do failAt at_ "type() expects an elementary type" let tname ← mStr (← mField (← mField arg "typeName") "name") let some bits := bitsOf tname | failAt at_ s!"unsupported type().max {tname}" - pure (.expr { pre := #[], expr := ex s!"literal {2 ^ bits - 1}" }) + pure (.expr { pre := #[], expr := .literal (2 ^ bits - 1) }) -private partial def projectMember (id : Nat) (pre : Array String) (member : String) (at_ : Json) : M Val := do +private partial def projectMember (id : Nat) (pre : Array Stmt) (member : String) (at_ : Json) : M Val := do let some mem := (← get).mems.find? id | failAt at_ "unknown memory parameter" let some idx := mem.members.findIdx? (fun p => p.1 == member) | failAt at_ s!"{member} is not a member of {mem.structName}" @@ -575,7 +533,7 @@ private partial def projectMember (id : Nat) (pre : Array String) (member : Stri noteProjection { parameter := mem.param, member := member, structName := mem.structName, headWord := head, modelParam := modelParam } - pure { pre, expr := ex s!"param {leanStr modelParam}" } + pure { pre, expr := .param modelParam } private partial def lowerBinary (j : Json) : M Val := do let op ← mStr (← mField j "operator") @@ -593,71 +551,72 @@ private partial def lowerBinary (j : Json) : M Val := do let some bits := bitsOf common | failAt j s!"unsupported mul type {common}" checkedMul bits left right | "/" => checkedDiv left right - | "<" => cmp "lt" left right - | ">" => cmp "gt" left right - | "<=" => cmp "le" left right - | ">=" => cmp "ge" left right - | "==" => cmp "eq" left right + | "<" => cmp .lt left right + | ">" => cmp .gt left right + | "<=" => cmp .le left right + | ">=" => cmp .ge left right + | "==" => cmp .eq left right | "!=" => - let v ← cmp "eq" left right - pure { v with expr := ex s!"logicalNot {v.expr}" } + let v ← cmp .eq left right + pure { v with expr := .logicalNot v.expr } | _ => failAt j s!"unsupported operator {op}" private partial def checkedSub (left right : Val) : M Val := do let a ← atom left let b ← atom right let dest ← fresh - let ok := assignBind dest (bin "sub" a.expr b.expr) - let ite := iteStmt (bin "lt" a.expr b.expr) overflowPanic ok - pure { pre := a.pre ++ b.pre |>.push (letBind dest (lit 0)) |>.push ite, expr := localName dest } + let ok := Stmt.assignVar dest (.sub a.expr b.expr) + let ite := iteStmt (.lt a.expr b.expr) overflowPanic ok + pure { pre := a.pre ++ b.pre |>.push (.letVar dest (.literal 0)) |>.push ite, expr := .localVar dest } private partial def checkedDiv (left right : Val) : M Val := do let a ← atom left let b ← atom right let dest ← fresh - let ok := assignBind dest (bin "div" a.expr b.expr) - let ite := iteStmt (bin "eq" b.expr (lit 0)) divPanic ok - pure { pre := a.pre ++ b.pre |>.push (letBind dest (lit 0)) |>.push ite, expr := localName dest } + let ok := Stmt.assignVar dest (.div a.expr b.expr) + let ite := iteStmt (.eq b.expr (.literal 0)) divPanic ok + pure { pre := a.pre ++ b.pre |>.push (.letVar dest (.literal 0)) |>.push ite, expr := .localVar dest } private partial def checkedAdd (bits : Nat) (left right : Val) : M Val := do let a ← atom left let b ← atom right let dest ← fresh - let sum := bin "add" a.expr b.expr - let ok := assignBind dest sum + let sum := Expr.add a.expr b.expr + let ok := Stmt.assignVar dest sum let cond := - if bits == 256 then bin "lt" sum a.expr - else bin "lt" (lit (2 ^ bits - 1)) sum - pure { pre := a.pre ++ b.pre |>.push (letBind dest (lit 0)) |>.push (iteStmt cond overflowPanic ok), expr := localName dest } + if bits == 256 then Expr.lt sum a.expr + else Expr.lt (.literal (2 ^ bits - 1)) sum + pure { pre := a.pre ++ b.pre |>.push (.letVar dest (.literal 0)) |>.push (iteStmt cond overflowPanic ok), + expr := .localVar dest } private partial def checkedMul (bits : Nat) (left right : Val) : M Val := do let a ← atom left let b ← atom right let dest ← fresh - let prod := bin "mul" a.expr b.expr - let ok := assignBind dest prod + let prod := Expr.mul a.expr b.expr + let ok := Stmt.assignVar dest prod -- Check the EVM-width product before the Solidity-width bound. For e.g. -- uint248, an overflowing 256-bit product can wrap below the uint248 bound. let bounded := if bits == 256 then ok - else iteStmt (bin "lt" (lit (2 ^ bits - 1)) prod) overflowPanic ok - let inner := iteStmt (bin "eq" (bin "div" prod a.expr) b.expr) bounded overflowPanic - let ite := iteStmt (bin "eq" a.expr (lit 0)) bounded inner - pure { pre := a.pre ++ b.pre |>.push (letBind dest (lit 0)) |>.push ite, expr := localName dest } + else iteStmt (.lt (.literal (2 ^ bits - 1)) prod) overflowPanic ok + let inner := iteStmt (.eq (.div prod a.expr) b.expr) bounded overflowPanic + let ite := iteStmt (.eq a.expr (.literal 0)) bounded inner + pure { pre := a.pre ++ b.pre |>.push (.letVar dest (.literal 0)) |>.push ite, expr := .localVar dest } -private partial def cmp (op : String) (left right : Val) : M Val := do +private partial def cmp (op : Expr → Expr → Expr) (left right : Val) : M Val := do let a ← atom left let b ← atom right - pure { pre := a.pre ++ b.pre, expr := ex s!"{op} {a.expr} {b.expr}" } + pure { pre := a.pre ++ b.pre, expr := op a.expr b.expr } private partial def lowerConditional (j : Json) : M Val := do let cond ← atom (← lowerExpr (← mField j "condition")) let yes ← lowerExpr (← mField j "trueExpression") let no ← lowerExpr (← mField j "falseExpression") let dest ← fresh - let thenB := yes.pre.push (assignBind dest yes.expr) - let elseB := no.pre.push (assignBind dest no.expr) - let ite := st s!"ite {cond.expr} {leanList thenB} {leanList elseB}" - pure { pre := cond.pre.push (letBind dest (lit 0)) |>.push ite, expr := ex s!"localVar {leanStr dest}" } + let thenB := yes.pre.push (.assignVar dest yes.expr) + let elseB := no.pre.push (.assignVar dest no.expr) + let ite := Stmt.ite cond.expr thenB.toList elseB.toList + pure { pre := cond.pre.push (.letVar dest (.literal 0)) |>.push ite, expr := .localVar dest } private partial def lowerCast (j : Json) : M Val := do let targetExpr ← mField j "expression" @@ -676,8 +635,8 @@ private partial def lowerCast (j : Json) : M Val := do if narrow then let a ← atom v let dest ← fresh - let bound := letBind dest (bin "bitAnd" a.expr (lit (2 ^ bits - 1))) - pure { pre := a.pre.push bound, expr := localName dest } + let bound := Stmt.letVar dest (.bitAnd a.expr (.literal (2 ^ bits - 1))) + pure { pre := a.pre.push bound, expr := .localVar dest } else pure v @@ -733,7 +692,7 @@ private partial def inlineFn (fnId : Nat) (args : Array Val) (at_ : Json) : M Va let rets ← mArr (← mField (← mField fn "returnParameters") "parameters") unless rets.size == 1 do failAt fn "only single-value helpers are inlined" let retName ← mStr (← mField rets[0]! "name") - let mut pre : Array String := #[] + let mut pre : Array Stmt := #[] let mut yul := savedYul for i in [:params.size] do let p := params[i]! @@ -784,7 +743,7 @@ private partial def convert (paramTy argTy : String) (v : Val) (at_ : Json) : M else failAt at_ s!"unsupported implicit conversion from {argTy} to {paramTy}" private partial def lowerHelper (stmts : Array Json) (retName : String) : M Val := do - let mut pre : Array String := #[] + let mut pre : Array Stmt := #[] let mut result : Option Val := none for s in stmts do if result.isSome then failAt s "statement after helper result" @@ -814,7 +773,7 @@ private partial def lowerAssembly (j : Json) (retName : String) : M Val := do failAt asg s!"Yul assigns {vname}, not the return name {retName}" pure { pre := #[], expr := ← lowerYul (← mField asg "value") } -private partial def lowerLocal (s : Json) : M (Array String) := do +private partial def lowerLocal (s : Json) : M (Array Stmt) := do let decls ← mArr (← mField s "declarations") unless decls.size == 1 do failAt s "only a single declaration is supported" if decls[0]!.isNull then failAt s "empty declaration" @@ -834,10 +793,10 @@ private partial def lowerLocal (s : Json) : M (Array String) := do else let v ← lowerExpr init let binding ← fresh - let expr := localName binding + let expr := Expr.localVar binding modify fun e => { e with values := e.values.insert id expr, yulNames := e.yulNames.insert name expr } - pure (v.pre.push (letBind binding v.expr)) + pure (v.pre.push (.letVar binding v.expr)) end @@ -871,14 +830,14 @@ private def bindRoot (fn : Json) : M (Array SrcParam) := do else if ty.startsWith "struct " then failAt p s!"unsupported location {loc} for {ty}" else - let some tySyn := paramTypeSyntax ty | failAt p s!"unsupported parameter type {ty}" + let some pty := paramType ty | failAt p s!"unsupported parameter type {ty}" modify fun e => - let values := e.values.insert id (ex s!"param {leanStr name}") - let scalarTy := e.scalarTy.insert id tySyn + let values := e.values.insert id (.param name) + let scalarTy := e.scalarTy.insert id pty { e with values, scalarTy } pure out -private def lowerRoot (fn : Json) : M (Array String × Array SrcParam) := do +private def lowerRoot (fn : Json) : M (Array Stmt × Array SrcParam) := do let rootId ← mNat (← mField fn "id") modify fun e => { e with stack := [rootId] } let srcParams ← bindRoot fn @@ -888,7 +847,7 @@ private def lowerRoot (fn : Json) : M (Array String × Array SrcParam) := do unless mods.isEmpty do failAt fn "modifiers are outside this slice" let some _ := field? fn "body" | failAt fn "function has no body" let stmts ← mArr (← mField (← mField fn "body") "statements") - let mut out : Array String := #[] + let mut out : Array Stmt := #[] let mut returned := false for s in stmts do if returned then failAt s "statement after root return" @@ -902,17 +861,17 @@ private def lowerRoot (fn : Json) : M (Array String × Array SrcParam) := do if ← mBool (← mField expr "isInlineArray") then failAt expr "inline arrays are outside this slice" let cs ← mArr (← mField expr "components") - let mut pres : Array String := #[] - let mut exprs : Array String := #[] + let mut pres : Array Stmt := #[] + let mut exprs : Array Expr := #[] for c in cs do if c.isNull then failAt expr "empty return component" let v ← atom (← lowerExpr c) pres := pres ++ v.pre exprs := exprs.push v.expr - out := out ++ pres |>.push (st s!"returnValues {leanList exprs}") + out := out ++ pres |>.push (.returnValues exprs.toList) else let v ← atom (← lowerExpr expr) - out := out ++ v.pre |>.push (st s!"returnValues {leanList #[v.expr]}") + out := out ++ v.pre |>.push (.returnValues [v.expr]) | kind => failAt s s!"unsupported statement {kind}" unless returned do failAt fn "an explicit root return is required" pure (out, srcParams) @@ -1046,9 +1005,8 @@ private def moduleText (pkgRoot : System.FilePath) (rel : String) : MetaM String unless ← path.pathExists do throwError "importer source missing: {path}" IO.FS.readFile path -private def renderFn (f : FnRec) : String := - let tys := leanList (f.paramTypes.map leanStr) - s!"({ "{ " }contract := {leanStr f.contract}, name := {leanStr f.name}, declId := {f.declId}, paramTypes := {tys}{ " }" } : Compiler.CompilationModel.SolidityImport.ImportedFunction)" +private def FnRec.toImported (f : FnRec) : ImportedFunction := + { contract := f.contract, name := f.name, declId := f.declId, paramTypes := f.paramTypes.toList } /-- Solidity spelling of a solc `typeString`: `struct Market memory` → `Market`. -/ private def sourceTypeName (typeString : String) : String := Id.run do @@ -1096,7 +1054,7 @@ private def selectFunction (contract functionName : String) (written : Array Str private def importSlice (pkgRoot projectRoot : System.FilePath) (entry contract functionName : String) - (written : Array String) (profile : Profile) : MetaM (String × String × String) := do + (written : Array String) (profile : Profile) : MetaM (CompilationModel × ImportReport) := do let compiler := pkgRoot / ".lake/solidity-import/solc-0.8.34" let solcSha ← verifyCompiler compiler let versionOut ← IO.Process.output { cmd := compiler.toString, args := #["--version"] } @@ -1159,7 +1117,7 @@ private def importSlice env := { env with currentFile := entry } let ((body, srcParams), env2) ← (lowerRoot fn).run env env := env2 - let mut modelParams : Array String := #[] + let mut modelParams : Array Param := #[] let mut modelParamNames : Array String := #[] for p in srcParams do if env.mems.contains p.id then @@ -1167,41 +1125,40 @@ private def importSlice let mut seen := false for (member, ty) in mem.members do if let some proj := env.projections.find? (fun q => q.parameter == p.name && q.member == member) then - let some tySyn := paramTypeSyntax ty + let some pty := paramType ty | (failAt fn s!"unsupported projected type {ty}").run' env if modelParamNames.contains proj.modelParam then (failAt fn s!"projected parameter name collision: {proj.modelParam}").run' env modelParamNames := modelParamNames.push proj.modelParam - modelParams := modelParams.push - s!"({ "{ " }name := {leanStr proj.modelParam}, ty := {tySyn}{ " }" } : Compiler.CompilationModel.Param)" + modelParams := modelParams.push { name := proj.modelParam, ty := pty } seen := true unless seen do (failAt fn s!"struct parameter {p.name} was not read").run' env else - let some tySyn := env.scalarTy.find? p.id | throwError "missing parameter type" + let some pty := env.scalarTy.find? p.id | throwError "missing parameter type" if modelParamNames.contains p.name then (failAt fn s!"projected parameter name collision: {p.name}").run' env modelParamNames := modelParamNames.push p.name - modelParams := modelParams.push - s!"({ "{ " }name := {leanStr p.name}, ty := {tySyn}{ " }" } : Compiler.CompilationModel.Param)" - let retSyntax ← (do + modelParams := modelParams.push { name := p.name, ty := pty } + let returns ← (do let rets ← mArr (← mField (← mField fn "returnParameters") "parameters") - let mut out : Array String := #[] + let mut out : Array ParamType := #[] for r in rets do let ty ← mType r - let some tySyn := paramTypeSyntax ty | failAt r s!"unsupported return type {ty}" - out := out.push tySyn + let some pty := paramType ty | failAt r s!"unsupported return type {ty}" + out := out.push pty pure out) |>.run' env let mutability ← str (← field fn "stateMutability") let isView := mutability == "view" || mutability == "pure" - let mut fieldTerms : Array (Nat × String) := #[] + let mut fields : Array (Nat × Field) := #[] for name in env.referenced do let some info := env.fieldsByName.find? name | throwError "missing field {name}" - fieldTerms := fieldTerms.push (info.slot, info.term) - let sortedFields := (fieldTerms.qsort (fun a b => a.1 < b.1)).map (·.2) - let fnTerm := - s!"({ "{ " }name := {leanStr functionName}, params := {leanList modelParams}, returnType := none, returns := {leanList retSyntax}, isView := {isView}, body := {leanList body}{ " }" } : Compiler.CompilationModel.FunctionSpec)" - let modelName := contract ++ "_" ++ functionName ++ "_slice" - let modelTerm := - s!"({ "{ " }name := {leanStr modelName}, constructor := none, fields := {leanList sortedFields}, functions := [{fnTerm}]{ " }" } : Compiler.CompilationModel.CompilationModel)" + fields := fields.push (info.slot, info.field) + let sortedFields := (fields.qsort (fun a b => a.1 < b.1)).map (·.2) + let spec : FunctionSpec := + { name := functionName, params := modelParams.toList, returnType := none, + returns := returns.toList, isView, body := body.toList } + let model : CompilationModel := + { name := contract ++ "_" ++ functionName ++ "_slice", constructor := none, + fields := sortedFields.toList, functions := [spec] } let mut excluded : Array FnRec := #[] for (id, decl) in env.funs do unless env.included.any (·.declId == id) do @@ -1214,21 +1171,23 @@ private def importSlice contract := env.funContract.find? id |>.getD "", name, declId := id, paramTypes := tys } let sortedExcluded := excluded.qsort (fun a b => a.declId < b.declId) - let mut projTerms : Array String := #[] + let mut projections : Array ParamProjection := #[] for proj in env.projections do let ignored := match env.mems.toList.find? (fun pair => pair.2.param == proj.parameter) with - | some (_, mem) => mem.members.filterMap fun pair => + | some (_, mem) => mem.members.filterMap fun (pair : String × String) => if env.projections.any (fun q => q.parameter == proj.parameter && q.member == pair.1) - then none else some (leanStr pair.1) + then none else some pair.1 | none => #[] - projTerms := projTerms.push - s!"({ "{ " }parameter := {leanStr proj.parameter}, member := {leanStr proj.member}, structName := {leanStr proj.structName}, headWord := {proj.headWord}, modelParam := {leanStr proj.modelParam}, ignoredMembers := {leanList ignored}{ " }" } : Compiler.CompilationModel.SolidityImport.ParamProjection)" - let mut opaqueTerms : Array String := #[] + projections := projections.push + { parameter := proj.parameter, member := proj.member, structName := proj.structName, + headWord := proj.headWord, modelParam := proj.modelParam, ignoredMembers := ignored.toList } + let mut opaqueMembers : Array OpaqueMember := #[] for o in env.opaqueMembers do if env.referenced.contains o.field then - opaqueTerms := opaqueTerms.push - s!"({ "{ " }field := {leanStr o.field}, name := {leanStr o.name}, solcType := {leanStr o.solcType}, wordOffset := {o.wordOffset}, byteOffset := {o.byteOffset}{ " }" } : Compiler.CompilationModel.SolidityImport.OpaqueMember)" + opaqueMembers := opaqueMembers.push + { field := o.field, name := o.name, solcType := o.solcType, + wordOffset := o.wordOffset, byteOffset := o.byteOffset } let sourceRoot ← importerSourceRoot pkgRoot let importer ← moduleText sourceRoot "Compiler/SolidityImport/Import.lean" let coverage ← moduleText sourceRoot "Compiler/SolidityImport/Coverage.lean" @@ -1246,15 +1205,14 @@ private def importSlice ("Import.lean", Json.str importer), ("Coverage.lean", Json.str coverage), ("Report.lean", Json.str reportSrc)])] let digest := sha256Hex digestInput.compress.toUTF8 - let reportTerm := - s!"({ "{ " }importerVersion := {leanStr importerVersion}, solcLongVersion := {leanStr solcLongVersion}, solcSha256 := {leanStr solcSha}, settingsJson := {leanStr settings.compress}, sourceDigest := {leanStr digest}, contract := {leanStr contract}, rootFunction := {leanStr functionName}, includedFunctions := {leanList (env.included.map renderFn)}, excludedFunctions := {leanList (sortedExcluded.map renderFn)}, projections := {leanList projTerms}, storageFields := {leanList (env.referenced.map leanStr)}, opaqueMembers := {leanList opaqueTerms}, observesPanicPayload := false{ " }" } : Compiler.CompilationModel.SolidityImport.ImportReport)" - pure (modelTerm, reportTerm, digest) - -private def elabDef (name : Name) (type body : String) : CommandElabM Unit := do - let cmd := s!"def _root_.{name} : {type} :=\n{body}" - match Parser.runParserCategory (← getEnv) `command cmd "" with - | .error e => throwError e - | .ok stx => elabCommand stx + let report : ImportReport := + { importerVersion, solcLongVersion, solcSha256 := solcSha, settingsJson := settings.compress, + sourceDigest := digest, contract, rootFunction := functionName, + includedFunctions := (env.included.map FnRec.toImported).toList, + excludedFunctions := (sortedExcluded.map FnRec.toImported).toList, + projections := projections.toList, storageFields := env.referenced.toList, + opaqueMembers := opaqueMembers.toList, observesPanicPayload := false } + pure (model, report) /-- `function name(T₁, …, Tₙ)`: a function to import, with its Solidity parameter types. -/ syntax solidityRoot := &"function" ident "(" ident,* ")" @@ -1274,6 +1232,18 @@ private unsafe def evalProfileUnsafe (stx : Term) : TermElabM Profile := do @[implemented_by evalProfileUnsafe] private opaque evalProfile (stx : Term) : TermElabM Profile +private unsafe def evalModelUnsafe (n : Name) : TermElabM CompilationModel := + evalConstCheck CompilationModel ``CompilationModel n + +@[implemented_by evalModelUnsafe] +private opaque evalModel (n : Name) : TermElabM CompilationModel + +private unsafe def evalReportUnsafe (n : Name) : TermElabM ImportReport := + evalConstCheck ImportReport ``ImportReport n + +@[implemented_by evalReportUnsafe] +private opaque evalReport (n : Name) : TermElabM ImportReport + /-- The directory holding the importing package's `lakefile.lean`. -/ private def packageRoot : CommandElabM System.FilePath := do let mut pkg := (← IO.FS.realPath (← getFileName)).parent.getD "." @@ -1301,16 +1271,25 @@ def elabSolidityImport : CommandElab := fun stx => do let pkg ← packageRoot let project := if root.getString.startsWith "/" then System.FilePath.mk root.getString else pkg / root.getString - let (modelTerm, reportTerm, digest) ← liftTermElabM <| + let (model, report) ← liftTermElabM <| importSlice pkg project entry.getString contract.getId.toString fn.getId.toString written profile let ns := (← getCurrNamespace) ++ alias.getId - elabDef (ns ++ `model) "Compiler.CompilationModel.CompilationModel" modelTerm - elabDef (ns ++ `report) "Compiler.CompilationModel.SolidityImport.ImportReport" reportTerm - elabDef (ns ++ `sourceDigest) "String" (leanStr digest) - let covered := s!"theorem _root_.{ns ++ `covered} : Compiler.CompilationModel.SolidityImport.modelImportCovered {ns ++ `model} = true := by decide" - match Parser.runParserCategory (← getEnv) `command covered "" with - | .error e => throwError e - | .ok stx => elabCommand stx + let name (suffix : Name) := mkIdent (`_root_ ++ ns ++ suffix) + elabCommand (← `(def $(name `model) : Compiler.CompilationModel.CompilationModel := + $(← quoteModel model))) + elabCommand (← `(def $(name `report) : Compiler.CompilationModel.SolidityImport.ImportReport := + $(← quoteReport report))) + elabCommand (← `(def $(name `sourceDigest) : String := $(quote report.sourceDigest))) + elabCommand (← `(theorem $(name `covered) : + Compiler.CompilationModel.SolidityImport.modelImportCovered $(name `model) = true := by decide)) + -- The quoted definitions must denote exactly the values the importer built. + unless (← get).messages.hasErrors do + let back ← liftTermElabM <| evalModel (ns ++ `model) + unless toString (repr back) == toString (repr model) do + throwError "internal: the elaborated model differs from the imported value" + let backReport ← liftTermElabM <| evalReport (ns ++ `report) + unless backReport == report do + throwError "internal: the elaborated report differs from the imported value" if (← get).messages.hasErrors then throwError "Solidity import failed to check" catch e => diff --git a/Compiler/SolidityImport/Quote.lean b/Compiler/SolidityImport/Quote.lean new file mode 100644 index 0000000000..9b6bff6c10 --- /dev/null +++ b/Compiler/SolidityImport/Quote.lean @@ -0,0 +1,144 @@ +import Lean +import Compiler.SolidityImport.Coverage +import Compiler.SolidityImport.Report + +/-! +Term syntax for the `CompilationModel` values the importer builds. + +The importer lowers Solidity to ordinary `CompilationModel` values and quotes +them here, so the elaborated definitions are the same terms a user would write. +Only the fragment the importer emits is quoted; anything else is an internal +error rather than a silently dropped field. `Stmt.ecm` holds functions and +cannot be quoted at all. +-/ + +open Lean + +namespace Compiler.CompilationModel.SolidityImport + +variable {m : Type → Type} [Monad m] [MonadQuotation m] [MonadError m] + +private def list (xs : List Term) : m Term := `([$(xs.toArray),*]) + +private def optNat : Option Nat → m Term + | none => `(none) + | some n => `(some $(quote n)) + +partial def quoteExpr : Expr → m Term + | .literal n => `(Compiler.CompilationModel.Expr.literal $(quote n)) + | .localVar x => `(Compiler.CompilationModel.Expr.localVar $(quote x)) + | .param x => `(Compiler.CompilationModel.Expr.param $(quote x)) + | .blockTimestamp => `(Compiler.CompilationModel.Expr.blockTimestamp) + | .structMember f k x => do + `(Compiler.CompilationModel.Expr.structMember $(quote f) $(← quoteExpr k) $(quote x)) + | .structMember2 f k1 k2 x => do + `(Compiler.CompilationModel.Expr.structMember2 $(quote f) $(← quoteExpr k1) $(← quoteExpr k2) $(quote x)) + | .add a b => do `(Compiler.CompilationModel.Expr.add $(← quoteExpr a) $(← quoteExpr b)) + | .sub a b => do `(Compiler.CompilationModel.Expr.sub $(← quoteExpr a) $(← quoteExpr b)) + | .mul a b => do `(Compiler.CompilationModel.Expr.mul $(← quoteExpr a) $(← quoteExpr b)) + | .div a b => do `(Compiler.CompilationModel.Expr.div $(← quoteExpr a) $(← quoteExpr b)) + | .bitAnd a b => do `(Compiler.CompilationModel.Expr.bitAnd $(← quoteExpr a) $(← quoteExpr b)) + | .bitXor a b => do `(Compiler.CompilationModel.Expr.bitXor $(← quoteExpr a) $(← quoteExpr b)) + | .eq a b => do `(Compiler.CompilationModel.Expr.eq $(← quoteExpr a) $(← quoteExpr b)) + | .lt a b => do `(Compiler.CompilationModel.Expr.lt $(← quoteExpr a) $(← quoteExpr b)) + | .gt a b => do `(Compiler.CompilationModel.Expr.gt $(← quoteExpr a) $(← quoteExpr b)) + | .le a b => do `(Compiler.CompilationModel.Expr.le $(← quoteExpr a) $(← quoteExpr b)) + | .ge a b => do `(Compiler.CompilationModel.Expr.ge $(← quoteExpr a) $(← quoteExpr b)) + | .logicalNot a => do `(Compiler.CompilationModel.Expr.logicalNot $(← quoteExpr a)) + | e => throwError "internal: the importer cannot quote {repr e}" + +private def quotePanic : Verity.Core.PanicCode → m Term + | .arithmeticOverflow => `(Verity.Core.PanicCode.arithmeticOverflow) + | .divisionByZero => `(Verity.Core.PanicCode.divisionByZero) + +partial def quoteStmt : Stmt → m Term + | .letVar x v => do `(Compiler.CompilationModel.Stmt.letVar $(quote x) $(← quoteExpr v)) + | .assignVar x v => do `(Compiler.CompilationModel.Stmt.assignVar $(quote x) $(← quoteExpr v)) + | .ite c t e => do + `(Compiler.CompilationModel.Stmt.ite $(← quoteExpr c) + $(← list (← t.mapM quoteStmt)) $(← list (← e.mapM quoteStmt))) + | .panic code => do `(Compiler.CompilationModel.Stmt.panic $(← quotePanic code)) + | .returnValues vs => do + `(Compiler.CompilationModel.Stmt.returnValues $(← list (← vs.mapM quoteExpr))) + | _ => throwError "internal: the importer cannot quote this statement" + +private def quoteKey : MappingKeyType → m Term + | .bytes32 => `(Compiler.CompilationModel.MappingKeyType.bytes32) + | .address => `(Compiler.CompilationModel.MappingKeyType.address) + | .uint256 => `(Compiler.CompilationModel.MappingKeyType.uint256) + +private def quoteMember (s : StructMember) : m Term := do + let ty ← match s.ty with + | .uint256 => `(Compiler.CompilationModel.StructMemberType.uint256) + | .uint16 => `(Compiler.CompilationModel.StructMemberType.uint16) + | t => throwError "internal: the importer cannot quote {repr t}" + let packed ← match s.packed with + | none => `(none) + | some p => `(some ({ offset := $(quote p.offset), width := $(quote p.width) } : + Compiler.CompilationModel.PackedBits)) + `(({ name := $(quote s.name), ty := $ty, wordOffset := $(quote s.wordOffset), packed := $packed } : + Compiler.CompilationModel.StructMember)) + +def quoteField (f : Field) : m Term := do + unless !f.isTransient && f.packedBits.isNone && f.aliasSlots.isEmpty do + throwError "internal: field {f.name} has settings the importer does not emit" + let ty ← match f.ty with + | .mappingStruct k ms => do + `(Compiler.CompilationModel.FieldType.mappingStruct $(← quoteKey k) $(← list (← ms.mapM quoteMember))) + | .mappingStruct2 k1 k2 ms => do + `(Compiler.CompilationModel.FieldType.mappingStruct2 $(← quoteKey k1) $(← quoteKey k2) + $(← list (← ms.mapM quoteMember))) + | t => throwError "internal: the importer cannot quote {repr t}" + `(({ name := $(quote f.name), ty := $ty, slot := $(← optNat f.slot) } : Compiler.CompilationModel.Field)) + +def quoteParamType : ParamType → m Term + | .uint256 => `(Compiler.CompilationModel.ParamType.uint256) + | .address => `(Compiler.CompilationModel.ParamType.address) + | .bytes32 => `(Compiler.CompilationModel.ParamType.bytes32) + | .bool => `(Compiler.CompilationModel.ParamType.bool) + | .uintN n => `(Compiler.CompilationModel.ParamType.uintN $(quote n)) + | t => throwError "internal: the importer cannot quote {repr t}" + +/-- Quote a function built by the importer. Only the fields it sets are +emitted; `quoteModel` callers check the round trip against the value. -/ +def quoteFunction (f : FunctionSpec) : m Term := do + let params ← f.params.mapM fun p => do + `(({ name := $(quote p.name), ty := $(← quoteParamType p.ty) } : Compiler.CompilationModel.Param)) + `(({ name := $(quote f.name), params := $(← list params), returnType := none, + returns := $(← list (← f.returns.mapM quoteParamType)), isView := $(quote f.isView), + body := $(← list (← f.body.mapM quoteStmt)) } : Compiler.CompilationModel.FunctionSpec)) + +def quoteModel (model : CompilationModel) : m Term := do + `(({ name := $(quote model.name), constructor := none, + fields := $(← list (← model.fields.mapM quoteField)), + functions := $(← list (← model.functions.mapM quoteFunction)) } : + Compiler.CompilationModel.CompilationModel)) + +private def strings (xs : List String) : m Term := list (xs.map quote) + +def quoteReport (r : ImportReport) : m Term := do + let fn (f : ImportedFunction) : m Term := do + `(({ contract := $(quote f.contract), name := $(quote f.name), declId := $(quote f.declId), + paramTypes := $(← strings f.paramTypes) } : Compiler.CompilationModel.SolidityImport.ImportedFunction)) + let proj (p : ParamProjection) : m Term := do + `(({ parameter := $(quote p.parameter), member := $(quote p.member), structName := $(quote p.structName), + headWord := $(quote p.headWord), modelParam := $(quote p.modelParam), + ignoredMembers := $(← strings p.ignoredMembers) } : + Compiler.CompilationModel.SolidityImport.ParamProjection)) + let opq (o : OpaqueMember) : m Term := do + `(({ field := $(quote o.field), name := $(quote o.name), solcType := $(quote o.solcType), + wordOffset := $(quote o.wordOffset), byteOffset := $(quote o.byteOffset) } : + Compiler.CompilationModel.SolidityImport.OpaqueMember)) + `(({ importerVersion := $(quote r.importerVersion), solcLongVersion := $(quote r.solcLongVersion), + solcSha256 := $(quote r.solcSha256), settingsJson := $(quote r.settingsJson), + sourceDigest := $(quote r.sourceDigest), contract := $(quote r.contract), + rootFunction := $(quote r.rootFunction), + includedFunctions := $(← list (← r.includedFunctions.mapM fn)), + excludedFunctions := $(← list (← r.excludedFunctions.mapM fn)), + projections := $(← list (← r.projections.mapM proj)), + storageFields := $(← strings r.storageFields), + opaqueMembers := $(← list (← r.opaqueMembers.mapM opq)), + observesPanicPayload := $(quote r.observesPanicPayload) } : + Compiler.CompilationModel.SolidityImport.ImportReport)) + +end Compiler.CompilationModel.SolidityImport diff --git a/Compiler/SolidityImport/README.md b/Compiler/SolidityImport/README.md index d401a5240a..dac6abf8e9 100644 --- a/Compiler/SolidityImport/README.md +++ b/Compiler/SolidityImport/README.md @@ -66,9 +66,21 @@ proof of source-to-model fidelity, nor membership in the compiler's proven IR fragment. The translator and solc remain trusted; public ABI decoding, panic payloads, gas, and bytecode equivalence are outside this interface. -Lake does not track Solidity files read during elaboration. Consumers must -re-elaborate the import when checking external source freshness, compare the -fresh model/digest with the compiled import, and retain a reviewed inventory. +Declare the Solidity tree as a Lake input of the importing library, so editing +a source rebuilds the import: + +```lean +input_dir midnightSol where + path := "vendor/midnight/src" + filter := .extension "sol" +lean_lib MorphoMidnight where + needs := #[midnightSol] +``` + +The elaborated definitions are built as values and quoted, then evaluated back +and compared with the imported value. Consumers should still re-elaborate in +CI, compare the fresh model/digest with the compiled import, and retain a +reviewed inventory. The Morpho pilot demonstrates that workflow along with universal properties on `Denote.execStmtList`, rather than properties only on handwritten arithmetic. diff --git a/TRUST_ASSUMPTIONS.md b/TRUST_ASSUMPTIONS.md index ca31cac32f..b8158863c3 100644 --- a/TRUST_ASSUMPTIONS.md +++ b/TRUST_ASSUMPTIONS.md @@ -39,8 +39,9 @@ Only reached storage fields are decoded. Namespace-qualified output names and framed-JSON provenance are deterministic. Implicit root returns, named call arguments, virtual dispatch, and unsupported signed operations are rejected. The slice makes no dynamic ABI-head claim: that unrelated denotation extension -is not part of this change. Lake does not track the external Solidity reads, -so consumers must re-elaborate to verify freshness against a compiled import. +is not part of this change. Solidity sources are Lake inputs only when the +consumer declares them (`input_dir` + `needs`); consumers should also +re-elaborate in CI to verify freshness against a compiled import. ## Compilation Pipeline diff --git a/artifacts/trust_surface_report.json b/artifacts/trust_surface_report.json index aeec1da665..9489b0b058 100644 --- a/artifacts/trust_surface_report.json +++ b/artifacts/trust_surface_report.json @@ -167,9 +167,9 @@ ] }, "mechanisms": { - "@[implemented_by": 2, + "@[implemented_by": 4, "native_decide": 613, - "partial def": 193 + "partial def": 195 }, "notes": "native_decide trusts Lean.ofReduceBool or Lean 4.31 generated per-proof native_decide axioms + Lean.trustCompiler. Prose registry: AXIOMS.md, TRUST_ASSUMPTIONS.md (enforced by scripts/check_trust_surface_registry.py).", "schema_version": 1 diff --git a/lakefile.lean b/lakefile.lean index 523012a58e..260cd8e7ee 100644 --- a/lakefile.lean +++ b/lakefile.lean @@ -51,8 +51,15 @@ lean_lib «Contracts» where .andSubmodules `Contracts.ReentrancyRelyGuarantee ] +/-- Solidity read by `solidity_import`: editing it rebuilds the import. -/ +input_dir solidityImportSmokeSources where + path := "Contracts/SolidityImportSmoke" + filter := .extension "sol" + text := true + lean_lib «SolidityImportSmoke» where globs := #[.one `Contracts.SolidityImportSmoke.Smoke] + needs := #[solidityImportSmokeSources] lean_lib «Compiler» where globs := #[.andSubmodules `Compiler] diff --git a/scripts/solidity_differential/mutations.py b/scripts/solidity_differential/mutations.py index 37b6bf6e1a..0443456c44 100644 --- a/scripts/solidity_differential/mutations.py +++ b/scripts/solidity_differential/mutations.py @@ -8,8 +8,8 @@ from .engine import ROOT, WORKSPACE, HarnessError, command, write_json MUTANTS = { - "import-comparison": ("Compiler/SolidityImport/Import.lean", '| "<" => cmp "lt" left right', '| "<" => cmp "gt" left right'), - "import-field-slot": ("Compiler/SolidityImport/Import.lean", 'slot := some {slot}', 'slot := some {slot + 1}'), + "import-comparison": ("Compiler/SolidityImport/Import.lean", '| "<" => cmp .lt left right', '| "<" => cmp .gt left right'), + "import-field-slot": ("Compiler/SolidityImport/Import.lean", 'slot := some slot }', 'slot := some (slot + 1) }'), "denote-packed-mask": ("Verity/Core/Model/Denote.lean", '(2 ^ packed.width) - 1', '(2 ^ packed.width) - 2'), "denote-storage-read": ("Verity/Core/Model/Denote.lean", ' world.readSlot (wordNormalize slot)\n', ' world.readSlot (wordNormalize (slot + 1))\n'), } From 5d81ed48f356ec29451b39532bf7692318a2893b Mon Sep 17 00:00:00 2001 From: Thomas Marchand Date: Thu, 24 Sep 2026 15:52:05 +0200 Subject: [PATCH 14/22] Hash Quote.lean and Profile.lean into the Solidity import digest Both now shape the elaborated model or its solc settings, so a change to either must invalidate downstream provenance like the rest of the importer. --- Compiler/SolidityImport/Import.lean | 5 ++++- TRUST_ASSUMPTIONS.md | 3 ++- 2 files changed, 6 insertions(+), 2 deletions(-) diff --git a/Compiler/SolidityImport/Import.lean b/Compiler/SolidityImport/Import.lean index f22b26c27a..ea604b273f 100644 --- a/Compiler/SolidityImport/Import.lean +++ b/Compiler/SolidityImport/Import.lean @@ -1192,6 +1192,8 @@ private def importSlice let importer ← moduleText sourceRoot "Compiler/SolidityImport/Import.lean" let coverage ← moduleText sourceRoot "Compiler/SolidityImport/Coverage.lean" let reportSrc ← moduleText sourceRoot "Compiler/SolidityImport/Report.lean" + let quoteSrc ← moduleText sourceRoot "Compiler/SolidityImport/Quote.lean" + let profileSrc ← moduleText sourceRoot "Compiler/SolidityImport/Profile.lean" -- JSON framing prevents distinct file/signature lists from sharing a -- concatenation merely because their text contains separator newlines. let digestInput := Json.mkObj [ @@ -1203,7 +1205,8 @@ private def importSlice ("solcInput", input), ("importerSources", Json.mkObj [ ("Import.lean", Json.str importer), ("Coverage.lean", Json.str coverage), - ("Report.lean", Json.str reportSrc)])] + ("Report.lean", Json.str reportSrc), ("Quote.lean", Json.str quoteSrc), + ("Profile.lean", Json.str profileSrc)])] let digest := sha256Hex digestInput.compress.toUTF8 let report : ImportReport := { importerVersion, solcLongVersion, solcSha256 := solcSha, settingsJson := settings.compress, diff --git a/TRUST_ASSUMPTIONS.md b/TRUST_ASSUMPTIONS.md index b8158863c3..f14c6ce779 100644 --- a/TRUST_ASSUMPTIONS.md +++ b/TRUST_ASSUMPTIONS.md @@ -16,7 +16,8 @@ does not cover. `DenoteAgreement.execStmt_eq` still holds, including finishes as `.stop`. The proof denotation observes success versus revert. It does not observe the `PanicCode` payload. -The source digest hashes `Import.lean`, `Coverage.lean`, and `Report.lean` from +The source digest hashes `Import.lean`, `Coverage.lean`, `Report.lean`, +`Quote.lean`, and `Profile.lean` from the package that contains them: the Verity tree itself, or `.lake/packages/verity` when a downstream package elaborates the import. The pinned solc binary stays in the elaborating package's From 28bcb8ffaf0f017bbe635dd6b90809781a5bfe05 Mon Sep 17 00:00:00 2001 From: Thomas Marchand Date: Thu, 24 Sep 2026 16:03:26 +0200 Subject: [PATCH 15/22] Import several Solidity functions into one model `solidity_import` now accepts one `function` clause per root. Each root is lowered with fresh bindings, generated names and projections, so adding a root cannot change how another is lowered; all roots share one field list. The model is named after the contract (C_f_slice -> C), the report lists `roots`, and each projection records its function. The smoke contract gains `lossOf(bytes32)`. The mutation suite checks that importing f with lossOf leaves f byte-identical to importing it alone, and rejects duplicate and unknown roots; lossOf also runs A/B/C in CI. --- .github/workflows/verify.yml | 1 + Compiler/SolidityImport/Import.lean | 134 ++++++++++-------- Compiler/SolidityImport/Quote.lean | 5 +- Compiler/SolidityImport/Report.lean | 9 +- Contracts/SolidityImportSmoke/Slice.sol | 4 + .../differential-loss.json | 27 ++++ Contracts/SolidityImportSmoke/model.golden | 2 +- scripts/solidity_import_mutations.py | 36 +++++ 8 files changed, 151 insertions(+), 67 deletions(-) create mode 100644 Contracts/SolidityImportSmoke/differential-loss.json diff --git a/.github/workflows/verify.yml b/.github/workflows/verify.yml index 74b7cd384d..57077886d0 100644 --- a/.github/workflows/verify.yml +++ b/.github/workflows/verify.yml @@ -1248,6 +1248,7 @@ jobs: timeout-minutes: 10 run: | scripts/check_solidity_differential.sh --config Contracts/SolidityImportSmoke/differential.json --output .lake/import-differential/smoke --cases 128 --seed 2438 + scripts/check_solidity_differential.sh --config Contracts/SolidityImportSmoke/differential-loss.json --output .lake/import-differential/loss --cases 64 --seed 2438 scripts/check_solidity_differential.sh --programs 5 --cases 16 --seed 2438 --output .lake/import-differential/programs - name: Extended Solidity slice campaigns diff --git a/Compiler/SolidityImport/Import.lean b/Compiler/SolidityImport/Import.lean index ea604b273f..837a895ed0 100644 --- a/Compiler/SolidityImport/Import.lean +++ b/Compiler/SolidityImport/Import.lean @@ -1053,8 +1053,9 @@ private def selectFunction (contract functionName : String) (written : Array Str pure (fn, tys) private def importSlice - (pkgRoot projectRoot : System.FilePath) (entry contract functionName : String) - (written : Array String) (profile : Profile) : MetaM (CompilationModel × ImportReport) := do + (pkgRoot projectRoot : System.FilePath) (entry contract : String) + (roots : Array (String × Array String)) (profile : Profile) : + MetaM (CompilationModel × ImportReport) := do let compiler := pkgRoot / ".lake/solidity-import/solc-0.8.34" let solcSha ← verifyCompiler compiler let versionOut ← IO.Process.output { cmd := compiler.toString, args := #["--version"] } @@ -1113,52 +1114,76 @@ private def importSlice for item in items do let label ← str (← field item "label") env := { env with layoutItems := env.layoutItems.insert label item } - let (fn, paramTys) ← (selectFunction contract functionName written).run' env - env := { env with currentFile := entry } - let ((body, srcParams), env2) ← (lowerRoot fn).run env - env := env2 - let mut modelParams : Array Param := #[] - let mut modelParamNames : Array String := #[] - for p in srcParams do - if env.mems.contains p.id then - let some mem := env.mems.find? p.id | throwError "missing memory parameter {p.name}" - let mut seen := false - for (member, ty) in mem.members do - if let some proj := env.projections.find? (fun q => q.parameter == p.name && q.member == member) then - let some pty := paramType ty - | (failAt fn s!"unsupported projected type {ty}").run' env - if modelParamNames.contains proj.modelParam then - (failAt fn s!"projected parameter name collision: {proj.modelParam}").run' env - modelParamNames := modelParamNames.push proj.modelParam - modelParams := modelParams.push { name := proj.modelParam, ty := pty } - seen := true - unless seen do (failAt fn s!"struct parameter {p.name} was not read").run' env - else - let some pty := env.scalarTy.find? p.id | throwError "missing parameter type" - if modelParamNames.contains p.name then (failAt fn s!"projected parameter name collision: {p.name}").run' env - modelParamNames := modelParamNames.push p.name - modelParams := modelParams.push { name := p.name, ty := pty } - let returns ← (do - let rets ← mArr (← mField (← mField fn "returnParameters") "parameters") - let mut out : Array ParamType := #[] - for r in rets do - let ty ← mType r - let some pty := paramType ty | failAt r s!"unsupported return type {ty}" - out := out.push pty - pure out) |>.run' env - let mutability ← str (← field fn "stateMutability") - let isView := mutability == "view" || mutability == "pure" + let mut specs : Array FunctionSpec := #[] + let mut projections : Array ParamProjection := #[] + let mut signatures : Array Json := #[] + let mut rootIds : Array Nat := #[] + for (functionName, written) in roots do + let (fn, paramTys) ← (selectFunction contract functionName written).run' env + let rootId ← flexNat (← field fn "id") + if rootIds.contains rootId then throwError "function {contract}.{functionName} is imported twice" + rootIds := rootIds.push rootId + signatures := signatures.push (Json.mkObj + [("function", Json.str functionName), ("parameterTypes", Json.arr (paramTys.map Json.str))]) + -- Each root is lowered on its own: bindings, generated names and projections + -- do not leak between functions. Field layouts and the closure are shared. + env := { env with currentFile := entry, next := 0, values := RBMap.empty, paths := RBMap.empty, + mems := RBMap.empty, scalarTy := RBMap.empty, yulNames := RBMap.empty, + projections := #[] } + let ((body, srcParams), env2) ← (lowerRoot fn).run env + env := env2 + let mut modelParams : Array Param := #[] + let mut modelParamNames : Array String := #[] + for p in srcParams do + if env.mems.contains p.id then + let some mem := env.mems.find? p.id | throwError "missing memory parameter {p.name}" + let mut seen := false + for (member, ty) in mem.members do + if let some proj := env.projections.find? (fun q => q.parameter == p.name && q.member == member) then + let some pty := paramType ty + | (failAt fn s!"unsupported projected type {ty}").run' env + if modelParamNames.contains proj.modelParam then + (failAt fn s!"projected parameter name collision: {proj.modelParam}").run' env + modelParamNames := modelParamNames.push proj.modelParam + modelParams := modelParams.push { name := proj.modelParam, ty := pty } + seen := true + unless seen do (failAt fn s!"struct parameter {p.name} was not read").run' env + else + let some pty := env.scalarTy.find? p.id | throwError "missing parameter type" + if modelParamNames.contains p.name then (failAt fn s!"projected parameter name collision: {p.name}").run' env + modelParamNames := modelParamNames.push p.name + modelParams := modelParams.push { name := p.name, ty := pty } + let returns ← (do + let rets ← mArr (← mField (← mField fn "returnParameters") "parameters") + let mut out : Array ParamType := #[] + for r in rets do + let ty ← mType r + let some pty := paramType ty | failAt r s!"unsupported return type {ty}" + out := out.push pty + pure out) |>.run' env + let mutability ← str (← field fn "stateMutability") + let isView := mutability == "view" || mutability == "pure" + specs := specs.push + { name := functionName, params := modelParams.toList, returnType := none, + returns := returns.toList, isView, body := body.toList } + for proj in env.projections do + let ignored := + match env.mems.toList.find? (fun pair => pair.2.param == proj.parameter) with + | some (_, mem) => mem.members.filterMap fun (pair : String × String) => + if env.projections.any (fun q => q.parameter == proj.parameter && q.member == pair.1) + then none else some pair.1 + | none => #[] + projections := projections.push + { function := functionName, parameter := proj.parameter, member := proj.member, + structName := proj.structName, headWord := proj.headWord, modelParam := proj.modelParam, + ignoredMembers := ignored.toList } let mut fields : Array (Nat × Field) := #[] for name in env.referenced do let some info := env.fieldsByName.find? name | throwError "missing field {name}" fields := fields.push (info.slot, info.field) let sortedFields := (fields.qsort (fun a b => a.1 < b.1)).map (·.2) - let spec : FunctionSpec := - { name := functionName, params := modelParams.toList, returnType := none, - returns := returns.toList, isView, body := body.toList } let model : CompilationModel := - { name := contract ++ "_" ++ functionName ++ "_slice", constructor := none, - fields := sortedFields.toList, functions := [spec] } + { name := contract, constructor := none, fields := sortedFields.toList, functions := specs.toList } let mut excluded : Array FnRec := #[] for (id, decl) in env.funs do unless env.included.any (·.declId == id) do @@ -1171,17 +1196,6 @@ private def importSlice contract := env.funContract.find? id |>.getD "", name, declId := id, paramTypes := tys } let sortedExcluded := excluded.qsort (fun a b => a.declId < b.declId) - let mut projections : Array ParamProjection := #[] - for proj in env.projections do - let ignored := - match env.mems.toList.find? (fun pair => pair.2.param == proj.parameter) with - | some (_, mem) => mem.members.filterMap fun (pair : String × String) => - if env.projections.any (fun q => q.parameter == proj.parameter && q.member == pair.1) - then none else some pair.1 - | none => #[] - projections := projections.push - { parameter := proj.parameter, member := proj.member, structName := proj.structName, - headWord := proj.headWord, modelParam := proj.modelParam, ignoredMembers := ignored.toList } let mut opaqueMembers : Array OpaqueMember := #[] for o in env.opaqueMembers do if env.referenced.contains o.field then @@ -1200,8 +1214,7 @@ private def importSlice ("importerVersion", Json.str importerVersion), ("solcVersion", Json.str solcLongVersion), ("contract", Json.str contract), - ("function", Json.str functionName), - ("parameterTypes", Json.arr (paramTys.map Json.str)), + ("functions", Json.arr signatures), ("solcInput", input), ("importerSources", Json.mkObj [ ("Import.lean", Json.str importer), ("Coverage.lean", Json.str coverage), @@ -1210,7 +1223,7 @@ private def importSlice let digest := sha256Hex digestInput.compress.toUTF8 let report : ImportReport := { importerVersion, solcLongVersion, solcSha256 := solcSha, settingsJson := settings.compress, - sourceDigest := digest, contract, rootFunction := functionName, + sourceDigest := digest, contract, roots := (roots.map (·.1)).toList, includedFunctions := (env.included.map FnRec.toImported).toList, excludedFunctions := (sortedExcluded.map FnRec.toImported).toList, projections := projections.toList, storageFields := env.referenced.toList, @@ -1267,15 +1280,14 @@ def elabSolidityImport : CommandElab := fun stx => do let profile ← liftTermElabM <| evalProfile prof unless profile.solc == solcLongVersion do throwError "this importer is pinned to solc {solcLongVersion}, not {profile.solc}" - let #[fnStx] := roots - | throwError "importing several functions is not supported yet" - let `(solidityRoot| function $fn ( $tys,* )) := fnStx | throwUnsupportedSyntax - let written := tys.getElems.map (·.getId.toString (escape := false)) + let roots ← roots.mapM fun root => do + let `(solidityRoot| function $fn ( $tys,* )) := root | throwUnsupportedSyntax + pure (fn.getId.toString, tys.getElems.map (·.getId.toString (escape := false))) let pkg ← packageRoot let project := if root.getString.startsWith "/" then System.FilePath.mk root.getString else pkg / root.getString let (model, report) ← liftTermElabM <| - importSlice pkg project entry.getString contract.getId.toString fn.getId.toString written profile + importSlice pkg project entry.getString contract.getId.toString roots profile let ns := (← getCurrNamespace) ++ alias.getId let name (suffix : Name) := mkIdent (`_root_ ++ ns ++ suffix) elabCommand (← `(def $(name `model) : Compiler.CompilationModel.CompilationModel := diff --git a/Compiler/SolidityImport/Quote.lean b/Compiler/SolidityImport/Quote.lean index 9b6bff6c10..de67f7955e 100644 --- a/Compiler/SolidityImport/Quote.lean +++ b/Compiler/SolidityImport/Quote.lean @@ -121,7 +121,8 @@ def quoteReport (r : ImportReport) : m Term := do `(({ contract := $(quote f.contract), name := $(quote f.name), declId := $(quote f.declId), paramTypes := $(← strings f.paramTypes) } : Compiler.CompilationModel.SolidityImport.ImportedFunction)) let proj (p : ParamProjection) : m Term := do - `(({ parameter := $(quote p.parameter), member := $(quote p.member), structName := $(quote p.structName), + `(({ function := $(quote p.function), parameter := $(quote p.parameter), member := $(quote p.member), + structName := $(quote p.structName), headWord := $(quote p.headWord), modelParam := $(quote p.modelParam), ignoredMembers := $(← strings p.ignoredMembers) } : Compiler.CompilationModel.SolidityImport.ParamProjection)) @@ -132,7 +133,7 @@ def quoteReport (r : ImportReport) : m Term := do `(({ importerVersion := $(quote r.importerVersion), solcLongVersion := $(quote r.solcLongVersion), solcSha256 := $(quote r.solcSha256), settingsJson := $(quote r.settingsJson), sourceDigest := $(quote r.sourceDigest), contract := $(quote r.contract), - rootFunction := $(quote r.rootFunction), + roots := $(← strings r.roots), includedFunctions := $(← list (← r.includedFunctions.mapM fn)), excludedFunctions := $(← list (← r.excludedFunctions.mapM fn)), projections := $(← list (← r.projections.mapM proj)), diff --git a/Compiler/SolidityImport/Report.lean b/Compiler/SolidityImport/Report.lean index b793cb59aa..403d2e2b35 100644 --- a/Compiler/SolidityImport/Report.lean +++ b/Compiler/SolidityImport/Report.lean @@ -19,6 +19,8 @@ structure ImportedFunction where `headWord` is the member's index in the Solidity memory/ABI head, derived from the struct declaration order. This is not an ABI decoder. -/ structure ParamProjection where + /-- The imported function whose parameter is projected. -/ + function : String parameter : String member : String structName : String @@ -45,7 +47,8 @@ structure ImportReport where settingsJson : String sourceDigest : String contract : String - rootFunction : String + /-- The imported functions, in `solidity_import` order. -/ + roots : List String includedFunctions : List ImportedFunction excludedFunctions : List ImportedFunction projections : List ParamProjection @@ -60,13 +63,13 @@ structure ImportReport where def ImportReport.toText (r : ImportReport) : String := Id.run do let mut lines := [s!"importer {r.importerVersion}", s!"solc {r.solcLongVersion}", s!"solcSha256 {r.solcSha256}", s!"digest {r.sourceDigest}", - s!"contract {r.contract}", s!"root {r.rootFunction}", s!"settings {r.settingsJson}"] + s!"contract {r.contract}"] ++ r.roots.map (s!"root {·}") ++ [s!"settings {r.settingsJson}"] for fn in r.includedFunctions do lines := lines ++ [s!"include {fn.contract}.{fn.name} decl {fn.declId} params {fn.paramTypes}"] for fn in r.excludedFunctions do lines := lines ++ [s!"exclude {fn.contract}.{fn.name} decl {fn.declId} params {fn.paramTypes}"] for p in r.projections do - lines := lines ++ [s!"projection {p.parameter}.{p.member} head {p.headWord} as {p.modelParam} ignored {p.ignoredMembers}"] + lines := lines ++ [s!"projection {p.function} {p.parameter}.{p.member} head {p.headWord} as {p.modelParam} ignored {p.ignoredMembers}"] for name in r.storageFields do lines := lines ++ [s!"storage {name}"] for m in r.opaqueMembers do diff --git a/Contracts/SolidityImportSmoke/Slice.sol b/Contracts/SolidityImportSmoke/Slice.sol index 7a323657a2..63739702fd 100644 --- a/Contracts/SolidityImportSmoke/Slice.sol +++ b/Contracts/SolidityImportSmoke/Slice.sol @@ -50,4 +50,8 @@ contract C { : 0; return (uint128(post) - fee, uint128(postFee) - fee, fee); } + + function lossOf(bytes32 market) external view returns (uint128) { + return marketState[market].lossFactor; + } } diff --git a/Contracts/SolidityImportSmoke/differential-loss.json b/Contracts/SolidityImportSmoke/differential-loss.json new file mode 100644 index 0000000000..b9a5790772 --- /dev/null +++ b/Contracts/SolidityImportSmoke/differential-loss.json @@ -0,0 +1,27 @@ +{ + "project": ".", + "entry": "Slice.sol", + "contract": "C", + "function": "lossOf", + "param_types": [ + "bytes32" + ], + "variables": { + "market": 256, + "lossFactor": 128 + }, + "arguments": { + "market": "market" + }, + "storage": [ + { + "field": "marketState", + "keys": [ + "market" + ], + "members": { + "lossFactor": "lossFactor" + } + } + ] +} diff --git a/Contracts/SolidityImportSmoke/model.golden b/Contracts/SolidityImportSmoke/model.golden index 3073a1131d..8dcc7572d0 100644 --- a/Contracts/SolidityImportSmoke/model.golden +++ b/Contracts/SolidityImportSmoke/model.golden @@ -1,4 +1,4 @@ -{ name := "C_f_slice", +{ name := "C", contractId := 1, fields := [{ name := "position", ty := Compiler.CompilationModel.FieldType.mappingStruct2 diff --git a/scripts/solidity_import_mutations.py b/scripts/solidity_import_mutations.py index 92ecab95db..33e3f6cf16 100644 --- a/scripts/solidity_import_mutations.py +++ b/scripts/solidity_import_mutations.py @@ -292,6 +292,42 @@ def main() -> None: unless run 3 == some [9, 0, 0] do throw (IO.userError "small uint248 product changed") unless run (2^128) == none do throw (IO.userError "uint248 product overflow was accepted") ''') + two_roots = WORK / "base/TwoRoots.lean" + two_roots.write_text(HEADER.format(root=WORK / "base") + WITNESS + f''' +solidity_import both from "{WORK / "base"}" entry "Slice.sol" + using {{ evmVersion := "osaka", viaIR := true, optimizerRuns := some 466, bytecodeHash := "none" }} + contract C + function f(Mkt, bytes32, address) + function lossOf(bytes32) + +#eval show IO Unit from do + -- A second root must not change how the first one is lowered. + let single := imported.model.functions + match both.model.functions with + | [f, loss] => + unless toString (repr [f]) == toString (repr single) do + throw (IO.userError "adding a root changed the first function") + unless loss.name == "lossOf" && loss.params.map (·.name) == ["market"] do + throw (IO.userError s!"unexpected second root {{loss.name}}") + | _ => throw (IO.userError "expected two imported functions") + unless both.model.fields.map (·.name) == ["position", "marketState"] do + throw (IO.userError "roots do not share one field list") + unless both.report.roots == ["f", "lossOf"] do + throw (IO.userError "report roots changed") +''') + result = lean(two_roots) + if result.returncode: + raise SystemExit(result.stdout + result.stderr) + print("pass two-roots") + for name, extra, needle in [ + ("duplicate-root", " function f(Mkt, bytes32, address)\n function f(Mkt, bytes32, address)\n", "imported twice"), + ("unknown-root", " function nope(bytes32)\n", "no function C.nope(bytes32)")]: + bad = WORK / f"base/{name}.lean" + bad.write_text(HEADER.format(root=WORK / "base").replace(" function f(Mkt, bytes32, address)\n", extra)) + blob = lean(bad) + if blob.returncode == 0 or needle not in blob.stdout + blob.stderr: + raise SystemExit(f"{name}: expected rejection containing {needle!r}\n{blob.stdout}{blob.stderr}") + print(f"pass {name}") repeated = expect_success("deterministic", WORK / "base", witness=True) if digest_of(base) != digest_of(repeated): raise SystemExit("identical input did not reproduce the digest") From 83a1a6da21c9146973ba93b5dcb403f821e2495b Mon Sep 17 00:00:00 2001 From: Thomas Marchand Date: Thu, 24 Sep 2026 16:08:58 +0200 Subject: [PATCH 16/22] Document the Solidity importer in one place Merge Compiler/SolidityImport/README.md and TESTING.md into docs/SOLIDITY_IMPORT.md: usage and profiles, the supported subset, the four separately established statuses (importable, runs on Denote, compiles, covered by compiler proofs), the trust boundary, and A/B/C validation. The importer folder keeps a pointer. --- AUDIT.md | 4 +- Compiler/SolidityImport/README.md | 101 +---------------- Compiler/SolidityImport/TESTING.md | 117 -------------------- TRUST_ASSUMPTIONS.md | 6 +- docs/SOLIDITY_IMPORT.md | 168 +++++++++++++++++++++++++++++ scripts/setup_solc_import.py | 2 +- 6 files changed, 175 insertions(+), 223 deletions(-) delete mode 100644 Compiler/SolidityImport/TESTING.md create mode 100644 docs/SOLIDITY_IMPORT.md diff --git a/AUDIT.md b/AUDIT.md index a1667758da..1e1f1b5cdf 100644 --- a/AUDIT.md +++ b/AUDIT.md @@ -5,7 +5,7 @@ reviewable. Keep it synchronized with `TRUST_ASSUMPTIONS.md` and `AXIOMS.md` whenever semantics, trusted components, generated audit artifacts, or CI boundary checks change. -## Solidity function-slice import +## Solidity import `Compiler/SolidityImport` lowers one selected function and the callees solc's declaration ids reach into a `CompilationModel`. `returnValues` is observable @@ -36,7 +36,7 @@ A rejected import leaves no `.olean`. The downstream `morpho-midnight-verity` pilot proves all five CVL assertions on actual imported execution, with arbitrary state/oracle and mathematical integer ghosts. That proof adds no source-to-model equivalence claim. See -`Compiler/SolidityImport/README.md` for the supported subset and freshness checks. +`docs/SOLIDITY_IMPORT.md` for the supported subset and freshness checks. ## Current Audit State diff --git a/Compiler/SolidityImport/README.md b/Compiler/SolidityImport/README.md index dac6abf8e9..1a5f230b5f 100644 --- a/Compiler/SolidityImport/README.md +++ b/Compiler/SolidityImport/README.md @@ -1,102 +1,3 @@ # Solidity import -`solidity_import` elaborates a selected Solidity function and its reached -helpers into the ordinary Verity `CompilationModel`. It consumes a checksum-pinned -solc 0.8.34 AST and storage layout. There is no separate Solidity interpreter. - -```lean -import Compiler.SolidityImport.Import - -def build : Profile := - { evmVersion := "osaka", viaIR := true, optimizerRuns := some 466, bytecodeHash := "none" } - -solidity_import example from "Contracts/SolidityImportSmoke" entry "Slice.sol" using build - contract C - function f(Mkt, bytes32, address) -``` - -The command defines `example.model`, `example.report`, `example.sourceDigest`, -and the kernel theorem `example.covered`. It also works inside namespaces. -`example.report.toText` renders the included/excluded signatures, projections, -opaque members, settings, and source digest. The profile can also be written -inline: `using { evmVersion := "osaka", ... }`. Parameter types use their -Solidity spelling; a struct may be qualified (`IMidnight.Market`). - -Install the compiler with `python3 scripts/setup_solc_import.py`; a downstream -package can pass `--output .lake/solidity-import/solc-0.8.34`. Elaboration never -downloads a compiler. The `from` directory resolves relative to the importing -package's `lakefile.lean`, and imports use that directory's `remappings.txt`. - -## Supported boundary - -This is deliberately the subset needed by Midnight's `updatePositionView`, -not a claim to support arbitrary Solidity. - -| Construct | Lowering | -| --- | --- | -| Explicit scalar/tuple return | `returnValues`, preserving order | -| Local declarations and storage aliases | Hygienic scalar bindings or resolved read paths | -| One/two-key mappings to structs | solc slots, word offsets, and packed uint offsets | -| Read scalar member of a memory/calldata struct parameter | Explicit scalar projection; no public ABI decoder | -| Unsigned `+`, `-`, `*`, `/` | Word arithmetic with overflow/underflow/zero-divisor guards | -| Unsigned comparisons, equality | Scalar conditions | -| Narrowing casts | Bit masks, not overflow checks | -| Ternaries | Lazy `ite` branches | -| Resolved acyclic helper calls | Inlined bodies with separate local scopes | -| Single assignment to a named assembly return | `xor`, `mul`, `lt` expressions, as in `UtilsLib.min` | - -The root must return explicitly. Loops, state writes, external calls, modifiers, -recursion, virtual dispatch, named call arguments, signed operations, and -unrecognized reached constructs are rejected. Unsupported functions outside the -closure are recorded as excluded. Only reached storage fields are decoded; -opaque array/mapping members are reported and cannot be read. Scalar projections -are checked for parameter-name collisions. - -## Determinism and trust - -Function selection uses the contract, name, and full parameter-type list. Helper -resolution uses solc declaration IDs. Generated names are valid compiler identifiers and checked against source names -and potential parameter projections; helper-local environments are restored after inlining. Field and -function ordering is deterministic. The digest uses framed JSON containing the -complete solc input, selected signature, release identity, and importer sources. -The report records the checksum actually verified for the invoked compiler. - -A covered constructor has an explicit denotation arm. Coverage is **not** a -proof of source-to-model fidelity, nor membership in the compiler's proven IR -fragment. The translator and solc remain trusted; public ABI decoding, panic -payloads, gas, and bytecode equivalence are outside this interface. - -Declare the Solidity tree as a Lake input of the importing library, so editing -a source rebuilds the import: - -```lean -input_dir midnightSol where - path := "vendor/midnight/src" - filter := .extension "sol" -lean_lib MorphoMidnight where - needs := #[midnightSol] -``` - -The elaborated definitions are built as values and quoted, then evaluated back -and compared with the imported value. Consumers should still re-elaborate in -CI, compare the fresh model/digest with the compiled import, and retain a -reviewed inventory. -The Morpho pilot demonstrates that workflow along with universal properties on -`Denote.execStmtList`, rather than properties only on handwritten arithmetic. - -## Validation - -```sh -lake build SolidityImportSmoke -python3 scripts/solidity_import_mutations.py -``` - -The regression suite exercises helper/local-name hygiene, namespace use, -shadowed builtins, narrow multiplication overflow, repeated deterministic imports, -unrelated unsupported storage, explicit rejection -of named arguments and implicit returns, reached/unreached unsupported loops, -and detection of changes to arithmetic, field reads, layout, and return order. -The smoke interpreter checks do not replace kernel proofs. - -See [TESTING.md](TESTING.md) for reusable A/B/C execution, seeded fuzzing, -generated programs, reduction, mutations and the exact observation boundary. +See [docs/SOLIDITY_IMPORT.md](../../docs/SOLIDITY_IMPORT.md) for usage, the supported subset, the trust boundary and differential validation. diff --git a/Compiler/SolidityImport/TESTING.md b/Compiler/SolidityImport/TESTING.md deleted file mode 100644 index b200286de7..0000000000 --- a/Compiler/SolidityImport/TESTING.md +++ /dev/null @@ -1,117 +0,0 @@ -# Differential validation of Solidity slices - -The reusable engine lives in Verity. A consumer supplies a JSON fixture naming -its source/signature, scalar input domains, argument projections, storage recipes -and fixed regression corpus. No consumer arithmetic is reimplemented in the engine. - -## Execution contract - -Every case takes three paths: - -- **A:** original Solidity, compiled by checksum-pinned solc, deployed and called in Foundry. -- **B:** the imported `CompilationModel`, executed by the same `Denote.execStmtList` - used in the proofs, with Verity's pure Keccak engine for storage slots. -- **C:** that model compiled by the normal Verity compiler, compiled from Yul by - the pinned solc, deployed and called in the same Foundry harness. - -Python constructs source calldata with `eth-abi` and storage slots with -`eth-hash`; it does not share Verity's encoders or arithmetic. Ignored ABI members, -including dynamic arrays, receive deterministic noise. Unused packed bits, -adjacent storage words, keys and unrelated slots are varied too. Each EVM case -uses a restored snapshot. V1 accepts read-only slices, rejects any recorded -storage write, and compares the initialized/observed storage words on all paths. -This is not a test of arbitrary state-writing contracts. - -The comparator checks success/revert, exact scalar return words and observed -storage on A/B/C, plus exact return/revert bytes on A/C. B does not expose panic -payloads. Model calldata is the explicit scalar projection, not a claim that -Verity implements the complete source ABI. Gas and internal memory allocation -identity are not equivalence criteria. Resource exhaustion, invalid input, -unsupported compilation and tool failures are **harness errors**, never reverts. - -The root must have a public/external ABI, static scalar returns, no constructor -arguments and the Osaka profile. Missing support is rejected before comparison. -The driver refuses uncovered models and does not interpret unknown constructors -through Denote's fallback. `keccakMemorySlice` is unreachable in this whitelist. - -## Commands - -Run from a Lake workspace (Verity or a downstream project): - -```sh -scripts/check_solidity_differential.sh \ - --config Contracts/SolidityImportSmoke/differential.json \ - --output .lake/differential/smoke --seed 2438 --cases 128 - -scripts/check_solidity_differential.sh \ - --programs 5 --cases 16 --seed 2438 --output .lake/differential/programs - -scripts/check_solidity_differential.sh \ - --mutations --output .lake/differential/mutants -``` - -Downstream projects invoke the same script from their pinned Verity dependency. -The script installs its Python dependencies in the caller's `.lake` directory. -The ordinary proof build has no Python package dependency. - -Each generated expression is run directly and through hygienic renamed/helper -variants, including a scalar projection whose name collides with the temporary -prefix. The grammar exercises uint8/16/128/248/256 casts, checked arithmetic, -ternaries and helper composition. Independent source executions must agree -between metamorphic variants as well as between A/B/C. A metamorphic mismatch -is recorded in `metamorphic-divergence.json` with the differing source rows; -it is investigated directly rather than by the input reducer. - -## Failure evidence and reduction - -A campaign records the config, seed/values, materialized inputs, source closure, -solc inputs/outputs, generated Yul, both creation bytecodes, tool versions, -implementation/source hashes, import digest, logs and per-route results. -Fresh imports are checked against the recorded digest; changed implementation -or binary artifacts invalidate replay. The original workspace/toolchain is -required for replay; archived sources support auditing and reconstruction. - -```sh -scripts/check_solidity_differential.sh --replay --output .lake/differential/smoke -scripts/check_solidity_differential.sh --reduce --reduce-seconds 120 \ - --output .lake/differential/failing-campaign -``` - -The reducer preserves the mismatch category, not merely any failure. It reduces -inputs, and additionally reduces the typed expression tree for generated programs. -It does not delete text from arbitrary production Solidity. Original evidence is -retained; reduced cases go in a child directory. Reduction is budgeted and does -not claim a globally minimal counterexample. - -Mutation campaigns copy sources and create private copy-on-write build caches; -only dependency packages are shared. They compile actual importer and Denote -mutants. A changed comparison, wrong field slot, wrong packed mask and wrong -storage read must produce runtime divergences. A compile error/timeout is an -**invalid mutant**, not a detected semantic bug. Mutation outputs must be fresh. - -## Extension and rollout policy - -1. Keep the fixed source/case corpus and existing five Midnight proofs green. -2. Add positive, boundary, interaction and explicit-rejection cases for each new - supported construct. Add a runtime-killed mutant where the risk warrants it. -3. Run fixed seeds on PRs; longer seeded input/program/mutation campaigns on a - schedule and manually. Record every seed and publish failure artifacts. -4. Turn minimized counterexamples into reviewed, permanent regressions. -5. Keep importability, Denote coverage, compilability, compiler-proof coverage - and tested observables separate. No test count establishes source equivalence. - -The implementation sequence is shared protocol/A-B-C runner; input fuzzing and -consumer integration; generated programs/reducers; mutation checks and CI gates. -The initial PR/nightly budgets are 10/60 minutes excluding toolchain preparation; -CI timeouts are failures, not truncated passing campaigns. - -## EVMYulLean assessment - -The pinned dependency has a bytecode executor, `EvmYul.EVM.Ξ`, with explicit -execution exceptions including fuel exhaustion. The in-repo native harness is -primarily a Yul semantic/proof harness, not a drop-in runner for these deployed -bytecodes. A fourth path needs a concrete account/block/calldata setup, fork and -opcode conformance checks, independent return/revert/exception decoding, and -cross-checks against Foundry. It is not claimed as executed by this harness. -A/B/C is the implemented gate; adding D must not replace independent Foundry -execution or collapse an unsupported opcode into a contract revert. diff --git a/TRUST_ASSUMPTIONS.md b/TRUST_ASSUMPTIONS.md index f14c6ce779..a2b05a664b 100644 --- a/TRUST_ASSUMPTIONS.md +++ b/TRUST_ASSUMPTIONS.md @@ -2,11 +2,11 @@ This document states what Verity proves and what it still trusts. -## Solidity function-slice import +## Solidity import -`Compiler/SolidityImport/Import.lean` selects one function from a solc project, +`Compiler/SolidityImport/Import.lean` selects functions from a solc project, closes over the definitions solc's declaration ids actually reach, and elaborates -a `CompilationModel`. Execution of an accepted slice is +a `CompilationModel` (see `docs/SOLIDITY_IMPORT.md`). Execution of an accepted import is `Compiler.CompilationModel.Denote.execStmt`, restricted by `stmtListCovered` in `Compiler/SolidityImport/Coverage.lean`. That predicate is not `SupportedFunction`: the slice uses `panic`, `returnValues`, packed diff --git a/docs/SOLIDITY_IMPORT.md b/docs/SOLIDITY_IMPORT.md new file mode 100644 index 0000000000..e3797a27ec --- /dev/null +++ b/docs/SOLIDITY_IMPORT.md @@ -0,0 +1,168 @@ +# Solidity import + +`solidity_import` turns selected Solidity functions, and the helpers they +reach, into an ordinary Verity `CompilationModel`. The model runs on the same +`Denote` semantics as Verity's proofs, so theorems about it are theorems about +the imported code. The importer lives in `Compiler/SolidityImport/`; the +Midnight pilot (`lfglabs-dev/morpho-midnight-verity`) is the reference user. + +## Usage + +```lean +import Compiler.SolidityImport.Import + +def build : Profile := + { evmVersion := "osaka", viaIR := true, optimizerRuns := some 466, bytecodeHash := "none" } + +solidity_import example from "Contracts/SolidityImportSmoke" entry "Slice.sol" using build + contract C + function f(Mkt, bytes32, address) + function lossOf(bytes32) +``` + +- `from` is the Solidity project directory, relative to the importing + package's `lakefile.lean`; its `remappings.txt` resolves imports. `entry` is + the file declaring the contract. +- `using` takes any `Profile` value, named or inline + (`using { evmVersion := "osaka", ... }`). The settings go to solc and into the + digest; they should match how the audited bytecode is built. `solc` defaults + to the only accepted release, `0.8.34+commit.80d5c536`. +- Each `function` clause selects one root by name and Solidity parameter types. + A struct may be qualified (`IMidnight.Market`). Roots are lowered + independently and share one field list. + +The command defines `example.model`, `example.report` (`toText` renders the +reviewed inventory), `example.sourceDigest`, and the kernel theorem +`example.covered`. It works inside namespaces. + +Install the compiler once with `python3 scripts/setup_solc_import.py` (a +downstream package passes `--output .lake/solidity-import/solc-0.8.34`). +Elaboration never downloads a compiler; it checks the binary's SHA-256 before +and after running it. + +Declare the Solidity tree as a Lake input so editing a source rebuilds the +import: + +```lean +input_dir midnightSol where + path := "vendor/midnight/src" + filter := .extension "sol" +lean_lib MorphoMidnight where + needs := #[midnightSol] +``` + +## Supported Solidity + +This is deliberately the subset Midnight's `updatePositionView` needs, not a +claim to support arbitrary Solidity. + +| Construct | Lowering | +| --- | --- | +| Explicit scalar/tuple return | `returnValues`, preserving order | +| Local declarations and storage aliases | Hygienic scalar bindings or resolved read paths | +| One/two-key mappings to structs | solc slots, word offsets, and packed uint offsets | +| Scalar member of a memory/calldata struct parameter | Explicit scalar projection; no ABI decoder | +| Unsigned `+`, `-`, `*`, `/` | Word arithmetic with overflow/underflow/zero-divisor panics | +| Unsigned comparisons, equality | Scalar conditions | +| Narrowing casts | Bit masks, not overflow checks | +| Ternaries | Lazy `ite` branches | +| Resolved acyclic helper calls | Inlined bodies with separate local scopes | +| Single assignment to a named assembly return | `xor`, `mul`, `lt`, as in `UtilsLib.min` | + +Each root must return explicitly. Loops, state writes, external calls, +modifiers, recursion, virtual dispatch, named call arguments, signed +operations, and any other construct reached from a root are rejected with +`file:line:column`, the construct, the reason, and the call path from the root +(`closure: C.f -> L.unused`). Functions outside the closure are listed as +excluded and do not block the import. Only reached storage fields are decoded; +array/mapping struct members are reported as opaque and cannot be read. + +## What each check establishes + +Four questions are answered separately; none implies the next. + +1. **Importable.** The command succeeds, or fails with the diagnostic above. + A partial model is never produced. +2. **Runs on Denote.** `x.covered` is a kernel proof that every constructor in + the model has an explicit denotation arm (not the catch-all). +3. **Compiles.** The differential engine compiles the model with the normal + Verity compiler and reports `compilable`/`compileError`. +4. **Covered by compiler proofs.** Not established by the importer. Compiled + bytecode is tested (path C below), not proved equivalent. + +## Trust boundary + +solc's AST and storage layout, and the lowering in `Import.lean`, are trusted. +The model is built as ordinary values and quoted (`Quote.lean`); after +elaboration the definitions are evaluated back and must equal those values. +The digest is SHA-256 over framed JSON of the complete solc input, the selected +signatures, the solc release, and the sources of `Import.lean`, +`Coverage.lean`, `Report.lean`, `Quote.lean` and `Profile.lean`. Panic payloads, +gas, public ABI decoding and bytecode equivalence are outside the model. + +Consumers should re-elaborate in CI, check the fresh model is `rfl`-equal to +the compiled import, and keep a reviewed inventory (`report.toText`) and a +golden `repr` of the model, as the pilot's `check_import.py` does. + +## Differential validation + +Tests supplement proofs; no test count establishes equivalence. Every case +runs three paths: + +- **A:** the original Solidity, compiled by the pinned solc, run in Foundry. +- **B:** the imported model, run by `Denote.execStmtList` (the proofs' + semantics) with Verity's pure Keccak for mapping slots. +- **C:** the model compiled by Verity to Yul, then by solc, run in Foundry. + +A consumer supplies a JSON fixture (see +`Contracts/SolidityImportSmoke/differential.json`): source and signature, +input domains, argument projections, storage recipes and a fixed corpus. +Python builds calldata with `eth-abi` and storage slots with `eth-hash`, +independently of Verity. Unused packed bits, neighbouring words and unrelated +slots get noise. Each EVM case runs from a restored snapshot and must not +write storage. + +A/B/C must agree on success/revert, return words and observed storage; A and C +must also agree on revert bytes. Timeouts, resource limits, unsupported +compilation and tool failures are harness errors, never reverts. + +```sh +scripts/check_solidity_differential.sh --config Contracts/SolidityImportSmoke/differential.json \ + --output .lake/d/smoke --seed 2438 --cases 128 # inputs +scripts/check_solidity_differential.sh --programs 5 --cases 16 --seed 2438 \ + --output .lake/d/programs # generated programs +scripts/check_solidity_differential.sh --mutations --output .lake/d/mutants +scripts/check_solidity_differential.sh --replay --output .lake/d/smoke +scripts/check_solidity_differential.sh --reduce --reduce-seconds 120 --output .lake/d/failing +``` + +- **Generated programs** cover uint8/16/128/248/256 casts, checked arithmetic, + ternaries and helpers. Each also runs renamed and helper-extracted variants + that must behave identically (`metamorphic-divergence.json` on mismatch). +- **Replay and reduction.** A campaign records seeds, inputs, sources, solc + input/output, Yul, bytecodes, tool versions and hashes. Replay refuses + changed artifacts. The reducer shrinks inputs (and generated expression + trees) while preserving the mismatch category. +- **Mutants** are real importer and Denote edits built in isolated copies. + Each must produce a runtime divergence; one that fails to compile is + invalid, not detected. +- **Importer regressions:** `python3 scripts/solidity_import_mutations.py` + checks the golden model (`--update-golden` after review), hygiene, + determinism, rejections with diagnostics, multi-root independence, and that + source changes move the witness results and the digest. + +CI runs fixed seeds on every PR (10 minutes) and longer input, program and +mutant campaigns nightly and on demand (60 minutes), archiving the evidence. +A timeout fails the job. + +## Adding a construct + +A new construct needs, in the same PR: positive, rejection, boundary and +interaction cases; an A/B/C fixture or generated-program coverage; a mutant +when it carries real risk; and the Lean proofs it enables. Keep the smoke +golden model and the Midnight proofs green, and turn every reduced +counterexample into a permanent regression. + +EVMYulLean has a bytecode executor (`EvmYul.EVM.Ξ`) that could become a fourth +path; it would need account/block setup, opcode conformance checks and +independent result decoding, and must not replace Foundry. diff --git a/scripts/setup_solc_import.py b/scripts/setup_solc_import.py index 301d1f2a9c..19883700b4 100644 --- a/scripts/setup_solc_import.py +++ b/scripts/setup_solc_import.py @@ -1,5 +1,5 @@ #!/usr/bin/env python3 -"""Install pinned solc 0.8.34 for the function-slice importer. +"""Install pinned solc 0.8.34 for the Solidity importer (`solidity_import`). The binary is written to `.lake/solidity-import/solc-0.8.34`. Lake elaboration never downloads the compiler. From cc88290abaf6ddc02e57156f53be8e0286bedd11 Mon Sep 17 00:00:00 2001 From: Thomas Marchand Date: Thu, 24 Sep 2026 16:22:54 +0200 Subject: [PATCH 17/22] Simplify the repository root and documentation - Move AXIOMS.md and TRUST_ASSUMPTIONS.md into docs/ and update every check, test fixture, CI path filter, CODEOWNERS entry and link that names them. - Delete files no check reads: AUDIT.md (dated journal; history keeps it), SUPPORTEDSPEC_CONVERSION.md, Dockerfile, .gitconfig, .profile, verity.svg, one-off reports/prompts/scopes, docs/archive, docs/roadmap, docs/parity, scripts/REFERENCE.md, the finished consolidation inventory, an unused fetch script, the 27-line docsync shims and their tests, and design screenshots. - Rewrite README (46 lines: guarantees, quick start, layout, review order), CONTRIBUTING (57 lines) and CLAUDE.md. lake build and make check pass; no relative markdown link dangles. --- .gitconfig | 0 .github/CODEOWNERS | 4 +- .github/ocr/rules.json | 7 +- .github/scripts/ocr-router.js | 4 +- .github/workflows/verify.yml | 4 - .profile | 5 - AUDIT.md | 823 ------------------ CLAUDE.md | 8 +- CONTRIBUTING.md | 335 +------ Compiler/CompileDriverCommon.lean | 2 +- Compiler/Linker.lean | 2 +- Compiler/Proofs/ArithmeticProfile.lean | 2 +- Compiler/Proofs/IRGeneration/Contract.lean | 2 +- .../Proofs/IRGeneration/IRStorageWord.lean | 7 +- .../Proofs/IRGeneration/SupportedSpec.lean | 2 +- Compiler/Proofs/MappingSlot.lean | 2 +- Compiler/Proofs/README.md | 2 +- .../Storage/MappingCoherentAllKeys.lean | 2 +- Compiler/Selectors.lean | 2 +- Dockerfile | 10 - README.md | 128 +-- SUPPORTEDSPEC_CONVERSION.md | 193 ---- StorageLayoutReport.lean | 2 +- Verity/EVM/Frame.lean | 2 +- artifacts/COMPILER_ARTIFACTS.md | 30 - artifacts/STORAGE_LAYOUT_SUMMARY.md | 2 +- artifacts/issue_1060_progress.json | 4 +- artifacts/trust_surface_report.json | 2 +- .../artifacts/screenshots/dark-code-page.png | Bin 3128481 -> 0 bytes .../screenshots/docs-article-cream.png | Bin 3078666 -> 0 bytes .../screenshots/home-desktop-cream.png | Bin 596121 -> 0 bytes .../screenshots/home-mobile-cream.png | Bin 383350 -> 0 bytes docs-site/content/capabilities.mdx | 2 +- docs-site/content/core.mdx | 4 +- docs-site/content/faq.mdx | 2 +- .../content/guides/linking-libraries.mdx | 2 +- docs-site/content/reentrancy.mdx | 2 +- docs-site/content/trust-model.mdx | 2 +- docs-site/content/verification.mdx | 2 +- docs-site/public/llms.txt | 2 +- docs/ARCHITECTURE_PACKAGE_SPLIT.md | 175 ---- docs/ARITHMETIC_PROFILE.md | 6 +- AXIOMS.md => docs/AXIOMS.md | 5 +- docs/C2_STORAGE_LENS_PART2_SCOPE.md | 72 -- docs/CI_RUNNERS.md | 3 - docs/CI_RUNNER_BENCHMARK_REPORT.md | 260 ------ docs/CI_RUNNER_HARDWARE_ANALYSIS.md | 311 ------- docs/DGX_SPARK_BOTTLENECK_REPORT.md | 67 -- docs/INTERPRETER_FEATURE_MATRIX.md | 2 +- docs/IR_STORAGE_UINT256_REFACTOR.md | 211 ----- docs/LANGUAGE_DESIGN_AXES.md | 150 ---- docs/PANIC_SURFACE_1999_SPEC.md | 107 --- docs/ROADMAP.md | 4 +- .../TRUST_ASSUMPTIONS.md | 36 +- docs/UNLINK_AUDIT_VERITY_CORE_PROMPT.md | 239 ----- docs/VERIFICATION_STATUS.md | 4 +- docs/archive/IR_STORAGE_PHASE1_PLAN.md | 79 -- docs/archive/IR_STORAGE_PHASE2_PLAN.md | 84 -- docs/archive/IR_STORAGE_PHASE3_PLAN.md | 91 -- docs/archive/NATIVE_EVMYULLEAN_DONE_GRAPH.md | 395 --------- .../NATIVE_EVMYULLEAN_FULL_TRANSITION_DONE.md | 362 -------- docs/archive/README.md | 12 - docs/parity/README.md | 41 - docs/parity/erc4337.md | 370 -------- docs/parity/lido.md | 372 -------- docs/parity/morpho.md | 260 ------ docs/roadmap/log.md | 27 - examples/external-libs/README.md | 2 +- scripts/README.md | 2 +- scripts/REFERENCE.md | 71 -- ...eck_axiomatized_primitive_boundary_sync.py | 27 - scripts/check_axioms.py | 4 +- scripts/check_bridge_coverage_sync.py | 4 +- ...terpreter_feature_boundary_catalog_sync.py | 27 - scripts/check_layer2_boundary_sync.py | 27 - scripts/check_linear_memory_boundary_sync.py | 27 - scripts/check_low_level_call_boundary_sync.py | 27 - scripts/check_mapping_slot_boundary.py | 2 +- scripts/check_solc_pin.py | 6 +- scripts/check_struct_mapping_surface_sync.py | 27 - scripts/check_trust_surface_registry.py | 2 +- scripts/consolidation-inventory.md | 275 ------ scripts/docsync.py | 7 +- scripts/fetch_prebuilt_compiler_artifacts.sh | 73 -- scripts/generate_storage_layout_report.py | 2 +- scripts/generate_trust_surface_report.py | 2 +- ...eck_axiomatized_primitive_boundary_sync.py | 84 -- scripts/test_check_axioms.py | 3 +- ...terpreter_feature_boundary_catalog_sync.py | 38 - scripts/test_check_layer2_boundary_sync.py | 101 --- .../test_check_linear_memory_boundary_sync.py | 102 --- ...test_check_low_level_call_boundary_sync.py | 84 -- .../test_check_struct_mapping_surface_sync.py | 158 ---- ...st_evmyullean_fork_conformance_workflow.py | 4 +- scripts/update_doc_numbers.py | 2 +- scripts/verify_sync_spec.json | 4 +- scripts/verify_sync_spec_source.py | 4 +- verity.svg | 20 - 98 files changed, 162 insertions(+), 6407 deletions(-) delete mode 100644 .gitconfig delete mode 100644 .profile delete mode 100644 AUDIT.md delete mode 100644 Dockerfile delete mode 100644 SUPPORTEDSPEC_CONVERSION.md delete mode 100644 artifacts/COMPILER_ARTIFACTS.md delete mode 100644 docs-site/artifacts/screenshots/dark-code-page.png delete mode 100644 docs-site/artifacts/screenshots/docs-article-cream.png delete mode 100644 docs-site/artifacts/screenshots/home-desktop-cream.png delete mode 100644 docs-site/artifacts/screenshots/home-mobile-cream.png delete mode 100644 docs/ARCHITECTURE_PACKAGE_SPLIT.md rename AXIOMS.md => docs/AXIOMS.md (99%) delete mode 100644 docs/C2_STORAGE_LENS_PART2_SCOPE.md delete mode 100644 docs/CI_RUNNER_BENCHMARK_REPORT.md delete mode 100644 docs/CI_RUNNER_HARDWARE_ANALYSIS.md delete mode 100644 docs/DGX_SPARK_BOTTLENECK_REPORT.md delete mode 100644 docs/IR_STORAGE_UINT256_REFACTOR.md delete mode 100644 docs/LANGUAGE_DESIGN_AXES.md delete mode 100644 docs/PANIC_SURFACE_1999_SPEC.md rename TRUST_ASSUMPTIONS.md => docs/TRUST_ASSUMPTIONS.md (95%) delete mode 100644 docs/UNLINK_AUDIT_VERITY_CORE_PROMPT.md delete mode 100644 docs/archive/IR_STORAGE_PHASE1_PLAN.md delete mode 100644 docs/archive/IR_STORAGE_PHASE2_PLAN.md delete mode 100644 docs/archive/IR_STORAGE_PHASE3_PLAN.md delete mode 100644 docs/archive/NATIVE_EVMYULLEAN_DONE_GRAPH.md delete mode 100644 docs/archive/NATIVE_EVMYULLEAN_FULL_TRANSITION_DONE.md delete mode 100644 docs/archive/README.md delete mode 100644 docs/parity/README.md delete mode 100644 docs/parity/erc4337.md delete mode 100644 docs/parity/lido.md delete mode 100644 docs/parity/morpho.md delete mode 100644 docs/roadmap/log.md delete mode 100644 scripts/REFERENCE.md delete mode 100644 scripts/check_axiomatized_primitive_boundary_sync.py delete mode 100644 scripts/check_interpreter_feature_boundary_catalog_sync.py delete mode 100644 scripts/check_layer2_boundary_sync.py delete mode 100644 scripts/check_linear_memory_boundary_sync.py delete mode 100644 scripts/check_low_level_call_boundary_sync.py delete mode 100644 scripts/check_struct_mapping_surface_sync.py delete mode 100644 scripts/consolidation-inventory.md delete mode 100755 scripts/fetch_prebuilt_compiler_artifacts.sh delete mode 100644 scripts/test_check_axiomatized_primitive_boundary_sync.py delete mode 100644 scripts/test_check_interpreter_feature_boundary_catalog_sync.py delete mode 100644 scripts/test_check_layer2_boundary_sync.py delete mode 100644 scripts/test_check_linear_memory_boundary_sync.py delete mode 100644 scripts/test_check_low_level_call_boundary_sync.py delete mode 100644 scripts/test_check_struct_mapping_surface_sync.py delete mode 100644 verity.svg diff --git a/.gitconfig b/.gitconfig deleted file mode 100644 index e69de29bb2..0000000000 diff --git a/.github/CODEOWNERS b/.github/CODEOWNERS index 72ea98aca1..6dd66d97dd 100644 --- a/.github/CODEOWNERS +++ b/.github/CODEOWNERS @@ -7,8 +7,8 @@ * @Th0rgal # Axioms and trust assumptions — any change weakens guarantees -AXIOMS.md @Th0rgal -TRUST_ASSUMPTIONS.md @Th0rgal +docs/AXIOMS.md @Th0rgal +docs/TRUST_ASSUMPTIONS.md @Th0rgal # Lean proof files — ensure no sorry regressions Verity/Proofs/ @Th0rgal diff --git a/.github/ocr/rules.json b/.github/ocr/rules.json index 3ae865edc8..f78dd47225 100644 --- a/.github/ocr/rules.json +++ b/.github/ocr/rules.json @@ -1,7 +1,7 @@ { "rules": [ { - "path": "{AUDIT.md,TRUST_ASSUMPTIONS.md,AXIOMS.md}", + "path": "docs/{TRUST_ASSUMPTIONS.md,AXIOMS.md}", "rule": "Treat trust-boundary documentation changes as high-risk. Check whether a code/proof change weakens, broadens, or silently depends on assumptions, axioms, admitted facts, trusted external semantics, compiler equivalence, or audit exclusions. Flag any mismatch between these docs and changed Lean/Solidity/Yul/Cairo code. Prefer concrete line-linked findings; do not comment on wording unless it changes the trust model.", "merge_system_rule": true }, @@ -50,9 +50,8 @@ "**/*.sol", "**/*.yul", "**/*.cairo", - "AUDIT.md", - "TRUST_ASSUMPTIONS.md", - "AXIOMS.md", + "docs/TRUST_ASSUMPTIONS.md", + "docs/AXIOMS.md", "README.md", "docs/**/*.md", ".github/**/*" diff --git a/.github/scripts/ocr-router.js b/.github/scripts/ocr-router.js index 981765f2d4..bf30d84fa9 100644 --- a/.github/scripts/ocr-router.js +++ b/.github/scripts/ocr-router.js @@ -1345,7 +1345,7 @@ function countCategories(included, supportedFiles) { function categorize(filePath) { const base = path.posix.basename(filePath); if (filePath.endsWith('.lean')) return 'lean'; - if (['AUDIT.md', 'TRUST_ASSUMPTIONS.md', 'AXIOMS.md'].includes(base)) return 'trust-doc'; + if (['TRUST_ASSUMPTIONS.md', 'AXIOMS.md'].includes(base)) return 'trust-doc'; if (filePath.startsWith('.github/')) return 'workflow-script'; if (/\.(sol|yul|cairo)$/.test(filePath)) return 'contract'; if (filePath === 'README.md' || (filePath.startsWith('docs/') && filePath.endsWith('.md'))) return 'doc'; @@ -1358,7 +1358,7 @@ function isSupported(filePath) { const base = path.posix.basename(filePath); return filePath.endsWith('.lean') || /\.(sol|yul|cairo)$/.test(filePath) || - ['AUDIT.md', 'TRUST_ASSUMPTIONS.md', 'AXIOMS.md', 'README.md'].includes(base) || + ['TRUST_ASSUMPTIONS.md', 'AXIOMS.md', 'README.md'].includes(base) || (filePath.startsWith('docs/') && filePath.endsWith('.md')) || filePath.startsWith('.github/') || /\.(py|sh|bash|js|ts|json|ya?ml|toml)$/.test(filePath); diff --git a/.github/workflows/verify.yml b/.github/workflows/verify.yml index 57077886d0..d50d08ae85 100644 --- a/.github/workflows/verify.yml +++ b/.github/workflows/verify.yml @@ -28,9 +28,7 @@ on: - 'lean-toolchain' - 'foundry.toml' - 'Makefile' - - 'AXIOMS.md' - 'README.md' - - 'TRUST_ASSUMPTIONS.md' pull_request: types: [opened, synchronize, reopened, closed] paths: @@ -56,9 +54,7 @@ on: - 'lean-toolchain' - 'foundry.toml' - 'Makefile' - - 'AXIOMS.md' - 'README.md' - - 'TRUST_ASSUMPTIONS.md' workflow_dispatch: inputs: clean_build: diff --git a/.profile b/.profile deleted file mode 100644 index f45f3b2193..0000000000 --- a/.profile +++ /dev/null @@ -1,5 +0,0 @@ - -export PATH="$HOME/.elan/bin:$PATH" - -export NVM_DIR="$HOME/.config/nvm" -[ -s "$NVM_DIR/nvm.sh" ] && \. "$NVM_DIR/nvm.sh" # This loads nvm diff --git a/AUDIT.md b/AUDIT.md deleted file mode 100644 index 1e1f1b5cdf..0000000000 --- a/AUDIT.md +++ /dev/null @@ -1,823 +0,0 @@ -# Audit Registry - -This file records trust-boundary changes and the evidence that keeps them -reviewable. Keep it synchronized with `TRUST_ASSUMPTIONS.md` and `AXIOMS.md` -whenever semantics, trusted components, generated audit artifacts, or CI -boundary checks change. - -## Solidity import - -`Compiler/SolidityImport` lowers one selected function and the callees solc's -declaration ids reach into a `CompilationModel`. `returnValues` is observable -on `DenoteState.observedReturnWords` and is mirrored in `SourceSemantics` and -`DenoteAgreement`. Accepted imports carry `covered`, proved by `decide` -against `modelImportCovered`. The smoke contract -`Contracts/SolidityImportSmoke` contains an unreached `for` loop; the import -still succeeds. The mutation harness checks the interpreter result of two witnesses on that -imported body: `(95, 5, 5)` and a revert. `smoke_step_credit` is a kernel proof -of the first packed `credit` read. It is not a proof of the whole body, and it -is not a proof that the model matches solc bytecode. - -The slice compiler pin is solc `0.8.34+commit.80d5c536`, installed by `python3 scripts/setup_solc_import.py`. The source -digest reads the importer modules from the Verity tree, or from -`.lake/packages/verity` when a downstream package elaborates the command. The -solc binary remains the elaborating package's `.lake/solidity-import/solc-0.8.34`. - -Evidence commands: `lake build SolidityImportSmoke` after the 0.8.34 compiler is -installed at `.lake/solidity-import/solc-0.8.34`, and -`python3 scripts/solidity_import_mutations.py`. The mutation script checks an -unreached `for`, rejection of that `for` once it is reachable, and detection of -a reached-helper change, a fee-factor change, a field change, a return-order -change, and a layout swap. It also covers hygienic helper locals, generated-name -collisions, namespaces, repeated deterministic imports, unrelated unsupported -layouts, and fail-closed rejection of named arguments and implicit returns. -A rejected import leaves no `.olean`. - -The downstream `morpho-midnight-verity` pilot proves all five CVL assertions on -actual imported execution, with arbitrary state/oracle and mathematical integer -ghosts. That proof adds no source-to-model equivalence claim. See -`docs/SOLIDITY_IMPORT.md` for the supported subset and freshness checks. - -## Current Audit State - -- Lean proof placeholders: 0 `sorry` in compiler/proof modules. -- Project-level Lean axioms: 0. See `AXIOMS.md`. -- Authoritative safe-body Yul runtime target: pinned `lfglabs-dev/EVMYulLean`. -- The previous reference-comparison modules have been removed from the live - proof tree; native EVMYulLean is the checked runtime boundary. -- Yul-to-bytecode compilation remains trusted through pinned `solc` 0.8.33. -- Consumer-declared intrinsics are consumer-owned trust boundaries. The CLZ - prototype keeps Verity at 0 project-level axioms and requires downstream - packages to document any assumed intrinsic obligation they declare. -- Gas safety is not modeled by the semantic preservation theorems. - -## Issue #1722: EVMYulLean Semantic Target - -Status: full semantic integration for safe compiler-produced bodies. - -### Event Emission Proof-Model Alignment (2026-06) - -Source-semantics `normalizeEventValue` (Compiler/Proofs/IRGeneration/SourceSemantics.lean) -now includes a `.newtypeOf _ baseType => normalizeEventValue baseType value` arm, mirroring -the compiler's newtype erasure in `normalizeEventWord` (Compiler/CompilationModel/EventAbiHelpers.lean). -This proof-model alignment ensures that source event values normalize consistently with -compiled event word normalization for scalar newtype parameters. Affects the event proof -path only (non-events statements are unchanged). - -New proof module `Compiler/Proofs/IRGeneration/GenericInduction/EventBridge.lean` bridges -scalar event emission semantics, discharging the `EventHeadStepSemanticBridgeCatalog.bridge` -obligation with per-statement step lemmas, memory wrapping facts, and normalized-word -equivalence theorems. Proof-only scaffolding; no compiler output changes. - -The current generic scalar-event slice is intentionally top-level only: supported -`emit` statements may appear as function-body heads with scalar parameters and at -most three indexed parameters. The contract-level scalar-events wrapper now lifts -the function-level proof over dispatch when callers supply the scalar-event -list-interface witnesses, and `ContractFeatureTest.lean` includes a top-level -emit smoke contract. Nested emits inside structural statements remain future -work, and the Yul/EndToEnd proof layers still exclude event/log semantics. - -### Compiler-Side Emit-Argument Scope Collection (2026-06) - -`collectStmtNames` (Compiler/CompilationModel/ValidationHelpers.lean) emit arm now returns -`collectExprListNames args` rather than `eventName :: collectExprListNames args`. Event -argument expressions now participate in scope-name collection for statement sequence -validation; the event name itself is resolved against the event table and does not enter -the identifier scope. Enables scope-aware event argument validation without shadowing -event name resolution. - -### Proof-Side Scratch Memory Wrapping (2026-06) - -Memory-access functions in the proof IR model now wrap byte/word offsets modulo `2^256` -to match compiled code's wrapping `add` builtin semantics. Affected functions: -- `memorySliceWords` (IRInterpreter.lean): word-reading offset wrapping -- `yulLogDataWords` (IRInterpreter.lean): log data extraction offset wrapping -- `writeEventSignatureScratchFrom`, `writeUnindexedEventScratchFrom` (SourceSemantics.lean): - scratch memory write offset wrapping - -Ensures proof-side scratch addressing matches compiled emit block's offset arithmetic -under EVM `Uint256` wrapping, preventing false misalignment in the semantic bridge. - -The EVMYulLean transition moved the safe-body EndToEnd runtime target from -Verity-owned Yul builtin scaffolding to native EVMYulLean runtime execution. -The current proof surface has: - -- 36 of 36 builtin bridge theorems proven. -- 0 admitted bridge lemmas. -- `smod` and `sar` bridge equivalences fully discharged. -- `compileStmtList_always_bridged` proven for `BridgedSafeStmts`. -- Public native EndToEnd wrappers whose runtime target is - `EvmYul.Yul.callDispatcher`. - -The external-call/function-table family -(`internalCall`, `internalCallAssign`, `externalCallBind`, and `ecm`) now has -function-table-aware closure scaffolding in -`Compiler/Proofs/YulGeneration/Backends/EvmYulLeanCallClosure.lean`. The -public surface — `BridgedSourceInternalCallStmt`, -`BridgedSourceExternalCallBindStmt`, `BridgedSourceEcmStmt` (with the -per-module `ECMBridgeable` obligation), and the corresponding -`compileStmt_*_bridged` / `compileStmtList_*_bridged` closure theorems — -discharges `BridgedStmts` against a `BridgedFunctionTable`. The composition -lemma `BridgedStmts_of_compileStmtList_append` lets concrete contracts -chain these closures with `compileStmtList_always_bridged` across an -arbitrary `pfx ++ sfx` split. End-to-end smoke proof in the same file. -Wiring of `SupportedFragment` / `SupportedSpec` for contracts that -actually use this family is the next milestone. - -## Reentrancy Rely-Guarantee Framework (2026-06) - -A new proof-level reentrancy framework, complementary to the runtime -`nonreentrant` transient-storage guard (#1893). Additive and axiom-free; it -keeps the "Project-level Lean axioms: 0" invariant above. - -- **New modules** (no `sorry`, no `axiom`, no `native_decide`): - - `Verity/Core/Invariant.lean` — state-polymorphic rely-guarantee library - (`Preserves` / `runSeq` / `runSeq_preserves`). - - `Verity/Core/Reentrancy.lean` — grounds it against the `Contract` monad: - `reentrantCall`, `ContractPreserves`, `ReentrancySpec` and the - whole-interleaving meta-theorem `ReentrancySpec.schedule_preserves`, plus a - forward-compatible multi-contract layer (`System` / `lift` / `Isys` / - `cross_contract_schedule_preserves`). -- **New semantic surface**: `Env.reenter : ContractState → ContractState` - (default `id`, the no-reentry case) on `Verity/Core/Semantics.lean`. -- **Worked example**: `Contracts/ReentrancyRelyGuarantee` machine-checks the - Midnight `take`/`liquidate` callback bug — the no-lock `take` admits a - permanent bad-debt liquidation; the locked `take` admits none for any - lock-respecting adversary. -- **Trust boundary**: author obligations (entrypoint-registry completeness, - adversary-model fidelity, invariant adequacy). See the - "Reentrancy Rely-Guarantee Framework" section in `TRUST_ASSUMPTIONS.md`. -- **CI state**: the example is registered in `test/property_manifest.json` and - classified proof-only in `test/property_exclusions.json` (abstract - state-transformer proofs, no compiled bytecode to property- or - differential-test); structure/Foundry-test exemptions are recorded in - `scripts/check_contract_structure.py`. `artifacts/verification_status.json` - and `docs/VERIFICATION_STATUS.md` now record 15 example contracts and +8 - proven theorems (all proof-only, so runtime coverage moves 90% → 88% by - construction while proven count rises 283 → 291). - -### Cross-Function Reentrancy Gate (fail-closed) - -A compilation-level gate that complements the proof framework above: -`validateReentrancyDisposition` -([Compiler/CompilationModel/Validation.lean](Compiler/CompilationModel/Validation.lean)), -a dedicated pass run after call well-formedness validation (and independent of -the single-function CEI check in `validateFunctionSpec`), -now **rejects** any non-`view`/`pure` function whose body makes a direct -external call unless it carries a sound reentrancy disposition. This closes the -cross-function reentrancy class (Midnight `take`/`liquidate`) at the toolchain -boundary: a CEI-clean function is no longer enough, because its external -callback still leaves a transiently-exploitable state live for a reentrant -sibling entrypoint. - -- **Accept-set** (sound only): `nonreentrant()` (runtime transient-storage - guard, #1893) or `reentrancy_trusted` (new metadata-only, unproven author - assertion — emits no code, no proof obligation, recorded as a trust boundary). -- **Deliberately rejected**: `cei_safe` and `allow_post_interaction_writes` - concern single-function CEI ordering only and do **not** satisfy the gate. -- **Internal-helper closure** (Bugbot HIGH on #2032): the `nonreentrant()` - transient guard is attached **only** at the external dispatch boundary - (`attachNonReentrantGuard`, #1893), so it is absent on the lock-free - internal-helper shadow the macro emits for direct intra-contract calls. A - *lock-only* `nonreentrant` function (no `reentrancy_trusted`) is therefore - **rejected when invoked as an internal helper** at macro lowering - (`ensureCallableAsInternalHelper`, - [Verity/Macro/Internal.lean](Verity/Macro/Internal.lean)); routing such a call - through the shadow would run the guarded body without the lock and silently - bypass its only protection. A callee that *also* carries `reentrancy_trusted` - is accepted (the assertion covers the lock-free internal path). This mirrors - the existing `internal nonreentrant()` rejection - (`NonreentrantInternalHelperRejected`, #1971). -- **CEISafety demoted**: `Compiler.Proofs.IRGeneration.CEISafety` - (`CEIProofBackedExecution`) remains a valid proof surface, but it certifies - *single-function* Checks-Effects-Interactions ordering — it is **not** a - reentrancy defense and is no longer presented as one. Cross-function - reentrancy safety is owned solely by this gate's accept-set - (`nonreentrant`/`reentrancy_trusted`); CEI is subordinate ordering hygiene. -- **New annotation surface**: `reentrancy_trusted` threads through the macro - (`Verity/Macro/Syntax.lean` → `Translate/Parsing.lean` → `Types.lean` → - `Translate.lean`) onto `FunctionDecl.reentrancyTrusted` / - `FunctionSpec.reentrancyTrusted` (defaulted `false`, backward compatible). -- **Regression evidence**: `Contracts/Smoke/SecurityCombos.lean` pairs - `ReentrancyDispositionRequired` (a CEI-clean `take` rejected by the gate, - pinned with `#guard_msgs`) against `ReentrancyDispositionDeclared` (the - identical body accepted once it declares `reentrancy_trusted`) — identical - bodies, opposite verdicts, the Midnight shape proven unable to pass - `#check_contract` undeclared. -- **Axiom-free**: the gate is a pure validation predicate; `AXIOMS.md` is - unchanged. Trust-boundary prose is in the "Cross-Function Reentrancy Gate" - section of `TRUST_ASSUMPTIONS.md`. - -## External-Call Journal (2026-08) - -- `ContractState.calls` (`Verity.ExternalCall` entries) is a new append-only, - defaulted field: a proof-side observable of external-call boundaries, not - EVM state. No semantic-preservation claim covers it. -- `DenoteExternalCalls.denoteCall` is unchanged; the journal is added by the - definitionally layered `denoteCallJournaled` / `denoteJournaled`, so all - previously proven world/gas/rollback laws hold verbatim. -- Source-level observation is `DenoteExternalCalls.externalCall : - AdversaryModel → CallSite → Contract ExternalCallResult` - (`Verity/Core/Model/ContractExternalCall.lean`), which reports - callee failure/revert in-band and never raises a monadic revert; - `externalCallRequireSuccess` opts into bubbling snapshot rollback. -- Semantic choices (journal survives caller-side rollback; adversary cannot - write the journal) are documented in `TRUST_ASSUMPTIONS.md` §"External-Call - Journal". -- **Axiom-free**: `AXIOMS.md` unchanged. - -## FunctionSpec Calls, ETH Value, Multi-Contract World (2026-08) - -- `Denote.evalExpr` / `execStmt` still map `Expr.call` and - `Stmt.externalCallBind` to `none` / `.revert`, so - `DenoteAgreement` stays definitional against `SourceSemantics`. - The widened fragment is `Verity.Core.Model.DenoteFunctionCalls`: - `evalExprCall` / `execExternalCallBind` take a `CallEnv` - (`AdversaryModel` + link-time target / value / siteId), debit - `selfBalance` only on a successful `call`, and journal the real - target and value. -- Base `withTransactionContext` is unchanged (keeps - `DenoteAgreement` / `_frame_holds` stable). Payable calls use - `withPayableCallContext`, which credits `selfBalance` with - `msg.value`. `MultiContract.withCallContext` does the same on - each hop. -- EDSL `Contracts.externalCallBind` stays name-keyed (`target`/`value` - = 0). `externalCallBindTo` records target and value and debits ETH - on success. Callee state is not in that single-world stub. -- `Verity.MultiContract` is a finite `Address → ContractState` world - with ETH-valued hops. The P-ETH-1 ensemble is - Bus → Gateway → Vault → (Lido | request). Model-plane only: those - names are not compiled contracts and not an L2 claim. -- Zero new axioms. - -## Execution-Backed External Call Frames (2026-08) - -- `MultiContract.CallFrame` retains distinct caller-before, callee-before, and - callee-entry states. `callEntry` checks distinct accounts, `call` kind, - target/address agreement, and sufficient caller balance before producing a - frame. -- `MultiContract.executeCall` consumes a `CalleeExecution`; its produced - control and returndata determine commit/rollback and the journal entry. - Success commits the caller debit and callee post-state. Failure/revert - preserve both snapshots (apart from the caller's append-only observation). -- `DenoteFunctionCalls.executeFunctionWithCalls` retains the FunctionSpec - post-world and execution control. `runFunctionInFrame` is the first - source-shaped adapter into the framed boundary; `callFunction` is the - official checked composition and does not accept a separately supplied - observation or post-state. The legacy `DenoteResult` projection is derived - from the same execution. -- This slice deliberately supports ordinary `call` only. `staticcall` and - `delegatecall` need distinct frame invariants before admission. -- Zero new axioms. - -## EDSL Executable Plane: Linked External Calls Journal (2026-08) - -- The EDSL executable stubs for linked external calls are no longer silent: - `Contracts.externalCallBind`, `Contracts.callResultWords`, - `Contracts.tryExternalCallWords`, and the `safeTransfer`/`safeApprove` - family now append a `Verity.ExternalCall` entry to `ContractState.calls` - (via `Contracts.linkedCallEntry`/`recordLinkedCall`). Previously they were - `pure ()`-shaped no-ops, so any executable-plane theorem "about" an - external call was vacuously insensitive to duplicated, omitted, reordered, - renamed, or argument-mutated calls. -- General linked externals are name-keyed (address bound at link time), so - `ExternalCall` gained a defaulted `name : String := ""` field; model-plane - entries (`DenoteExternalCalls.journalEntry`) leave it `""` and are otherwise - unchanged. Their executable-plane `siteId`/`target` are `0`. ERC-20 write - wrappers instead record the token address as `target` and the actual wrapper - arguments (excluding that target) as `calldata`; all retain the wrapper name. - Journal position records call order. -- `ExternalArg.toWords` is a canonical, content-preserving journal encoding: - scalars occupy one word, while arrays and byte arrays carry a length prefix - followed by every recursively encoded element/byte. It deliberately does not - claim byte-for-byte EVM ABI layout, but unlike the former size-only word it - distinguishes same-length content mutations. -- In-band results remain deterministic stubs (`externalCallStubWord`); - `callResultWords` and successful `tryExternalCallWords` with a supported - single-word result decode and return the same word recorded in journal - returndata. Aggregate/no-result executable stubs retain their inhabited - default, as do failures. Both report - `success := (name != "fail")` (`externalCallStubSuccess`), giving specs a - reserved name to exercise failure paths. `externalCallWords` (the pure - expression form) still returns the stub word without journaling — it is - not monadic and cannot observe state; this remaining gap is documented in - `TRUST_ASSUMPTIONS.md`. -- The model plane (`DenoteExternalCalls`, `ContractExternalCall`, - `ExternalCallResult.control`) is untouched; all its laws hold verbatim. -- Discriminating evidence: `Contracts/Smoke/ExternalCallObservability.lean` - separates duplicated/omitted/reordered/renamed/zeroed-arg and equal-length - dynamic-content mutants over arbitrary pre-states, pins returned-value / - returndata agreement and ERC-20 targets/calldata, and pins the - revert-rolls-back-journal behaviour of `Contract.run`. -- **Axiom-free**: `AXIOMS.md` unchanged. - -## Guarded Event Preservation + Checked Arithmetic Completion (2026-08) - -- `Compiler/Proofs/IRGeneration/GuardedScalarEvents.lean`: - `compile_preserves_semantics_guarded_with_scalar_events` closes final-result - event preservation (`encodeEvents source.events = ir.events`) for the - guarded whole-contract pipeline on the scalar-event fragment, by proving - the guarded source semantics collapses to the plain one when every - dispatched function is lock-free (which `SupportedSpecWithScalarEvents` - forces). Pure composition of the #2000 event lane and the #2314–#2317 - guarded family; no new semantic assumption, no axiom. -- The `notModeledEventEmission` trust slice and `--deny-event-emission` gate - are **kept**: they flag `rawLog`, which remains unmodeled. Known limit - (documented in `TRUST_ASSUMPTIONS.md` §Event Emission): the slice does not - enumerate declared `Stmt.emit` sites outside the scalar fragment; that - boundary is enforced at the theorem support witness instead. -- `Verity/Core/Uint256.lean`: `checkedSub`/`checkedMul` + `subNoWrap`/`mulNoWrap` - complete the checked-arithmetic lane (#1993) alongside `checkedAdd`. -- Solidity-0.8-style unsigned arithmetic binds now lower to structured panic control - flow. `Verity/Macro/Translate/Expr.lean` emits an exact failure predicate, - `Stmt.panic .arithmeticOverflow` for overflow/underflow or - `Stmt.panic .divisionByZero` for division by zero, and then the arithmetic - result binding. Direct Lean helpers retain compatibility diagnostic strings; - macro-generated compilation bypasses them and emits canonical - `Panic(uint256)` ABI data rather than `Error(string)`. -- `Verity.Core.PanicCode` is closed over those two arithmetic codes. General - runtime `panic(code)` and generated enum guards such as `panic(0x21)` retain - the raw `Stmt.panicCode Expr` compatibility path. -- Helper discovery and the post-codegen Yul rewrite are fail-closed over the - generated shape: `Compiler/CompilationModel/UsageAnalysis.lean` requires an - exact pair of adjacent statements—a guarded typed panic followed by the - arithmetic result binding—which together form the guard/panic/arithmetic - sequence. `Compiler/CodegenCommon.lean` requires the typed direct-literal - compiled panic body and corresponding guard before replacing that pair with - a checked helper call. All four rewrites additionally require both operands - to be numeric literals or variable references; calls and compound operands - remain unchanged. `Stmt.unsafeYul` lowering surrounds raw fragments - with comment-only provenance markers; the same optimizer copies marked - regions verbatim and never matches across or descends into them, so - side-effecting handwritten operands cannot be evaluated a different number - of times by this peephole. - ECM uses distinct boundaries and is inspected automatically under the same - literal/variable operand checks, without author opt-in. Deployment and runtime - each require the actual six canonical helper definitions (four arithmetic, - two panic), exactly once at top level, with no conflicting bindings anywhere - in that section. ECM alone does not insert helpers. Region boundaries prevent - cross-region matches; malformed markers disable the pass. Reserved ECM marker - text in module output makes that module opaque. The ECM bridge wrapper proof - preserves syntactic support only, not optimizer semantic correctness. - Regression cases reject standalone panics, wrong panic codes, reversed - guards, mismatched operands, and raw lookalikes; generated-Yul checks cover - all four helper calls, selector `0x4e487b71`, codes `0x11`/`0x12`, and - `revert(0, 36)`. - The `compiler-regressions` CI job explicitly builds - `Compiler.PanicCodeRegressionTest`; the workflow sync specification requires - that step so it cannot be silently removed. -- `Compiler.Proofs.IRGeneration.execIRStmts_solidityPanicPayload` in `PanicPayloadIR` - proves the abstract IR-interpreter memory/revert result for every in-range - panic code, with typed specializations for both arithmetic constructors. - This result does not retain the revert offset, length, or bytes. - `Compiler.Proofs.YulGeneration.observePanicPayloadBytes_solidityPanicPayload` - in `PanicPayloadBytes` separately proves that the emitted AST returns exactly - the four selector bytes and the 32-byte code word, independently of initial - memory. Its size theorem proves 36 returned bytes. The byte observer uses - EVMYulLean's `mstore` and `evmRevert`; regressions distinguish empty, - truncated, and wrong-offset reverts. This local instruction-sequence proof - does not establish full native Yul execution or solc bytecode correctness. - The generic whole-contract `SupportedSpec` still excludes typed and raw panic - statements through the typed-revert effect surface, and the - `CodegenCommon` peephole rewrite is guarded by structural matching and tests - rather than an end-to-end preservation theorem. Those remaining boundaries - are recorded in `TRUST_ASSUMPTIONS.md` and `docs/ARITHMETIC_PROFILE.md`. -- **Axiom set unchanged**: this slice adds no `axiom` or `sorry` and leaves - `solidityMappingSlot_injective` as the single active project-level axiom. - The net increase in `native_decide` feature tests is tracked in - `artifacts/trust_surface_report.json`; `native_decide` is a documented - trusted reduction mechanism, not a project-level arithmetic axiom. - -## Returndata Surface (2026-08) - -> **Superseded in part by *First-Class Returndata Buffer (2026-08)* below.** The -> constant-zero model recorded here was sound only while the admitted fragment -> issued no call-family instruction. PRs #2398/#2399 added external-call oracle -> lanes, so `returndatasize()` is now read from a real buffer. The -> `returndataCopy` posture and the surface-gating notes below still hold. - -- `Expr.returndataSize` and `Stmt.returndataCopy` are now executed by the - source interpreters, the compiler-free denotation, and the IR interpreter - instead of falling through to an unmodeled revert. The model rests on - EIP-211: a frame's returndata buffer starts empty and is only refilled by a - call-family instruction, and the admitted fragment issues none. So - `evalExpr .returndataSize = some 0`, and `returndataCopy dst src sz` - continues only when `src + sz = 0` and otherwise reverts, matching the EVM's - exceptional halt on `src + size > returndatasize`. -- The generic proof fragment admits only the zero-extent shape, - `SupportedStmtList.returndataCopyEmptySingle` for - `Stmt.returndataCopy dst (.literal 0) (.literal 0)`. Because that copy writes - nothing, it needs no linear-memory model and preserves - `runtimeStateMatchesIR` definitionally. -- `Expr.returndataSize` is now part of `ExprCompileCore`: its compilation to - `returndatasize()` is proved against the same no-call invariant, so it can - occur in generic-fragment expressions without admitting a call-family - instruction or non-empty returndata. -- Admitting that constructor required dropping `.returndataSize` from - `exprTouchesUnsupportedCoreSurface` and `exprTouchesUnsupportedContractSurface`, - the last two gates that still rejected it. The call, state, helper, foreign, - and low-level gates already returned `false` for it. The call-family - constructors (`.call`, `.staticcall`, `.delegatecall`, `.externalCall`, - `.internalCall`) stay gated, which is what keeps the no-call invariant that - makes `returndatasize()` zero. -- `stmtTouchesUnsupported*Surface` previously returned `false` for - `.returndataCopy` without recursing into its subexpressions. That was sound - only while the statement always reverted; the ten recursive predicates now - mirror `calldatacopy` and recurse. `StmtListScopeCore` / - `StmtListScopeDiscipline` gained matching constructors, since the case is no - longer vacuous. -- No new axiom and no new trust assumption: this narrows an unsupported - surface rather than assuming one. `Expr.returndataOptionalBoolAt`, - `Stmt.revertReturndata`, `extcodesize`, and `externalCallBind` remain - unmodeled. - -## First-Class Returndata Buffer (2026-08) - -- Supersedes the "no-call invariant" justification recorded under *Returndata - Surface* above. That section rested on the admitted fragment issuing no - call-family instruction, so a frame's EIP-211 buffer stayed empty and - `returndatasize()` could be modeled as the constant `0`. Issue #2084 - (PRs #2398, #2399) added the external-call oracle lanes - `Stmt.externalCallBind`, `Stmt.tryExternalCallBind` and `Stmt.ecm` to the - source semantics and the compiler-free denotation, which invalidated that - premise: a modeled call can now return words, and the constant `0` would have - been an unsound read of the resulting frame. -- `Verity.ContractState` and `IRState` each gained a `returndata : List Nat` - field holding the callee's returned words. All three oracle lanes install the - receipt's `returnValues` through `SourceSemantics.returndataAfterCall`, on the - failure path as well as the success path, matching EIP-211: the buffer is - replaced by every call-family instruction regardless of outcome. -- `ContractState.returndataSize` is `32 * returndata.length` normalized to a - word, and `ContractState.returndataOptionalBool` reproduces the - `or(eq(returndatasize(), 0), and(eq(returndatasize(), 32), eq(mload(out), 1)))` - shape the compiler emits for ERC-20 style callees. `Expr.returndataSize` and - `Expr.returndataOptionalBoolAt` now denote those functions at all three planes - (`SourceSemantics.evalExpr` / `evalExprWithHelpers`, `Denote.evalExpr`, and - `evalIRExpr` via the new `returndatasize` builtin branch), and the value-bound - induction discharges both from `Uint256.isLt` rather than from a constant. -- `runtimeStateMatchesIR` and `constructorRuntimeStateMatchesIR` gained an - eighteenth conjunct, `state.returndata = runtime.world.returndata`, exposed as - `runtimeStateMatchesIR_returndata`. `initialIRStateForTx` seeds it from the - initial world. `evalIRExpr_returndataSize_of_runtimeStateMatchesIR` and - `eval_compileExpr_returndataOptionalBoolAt_of_compiled` are reproved against - the real buffer instead of against `some 0`. -- `execStmt_ecm_static_preserves_world_modulo_memory_calls_and_returndata` is - the renamed static-ECM preservation lemma; the buffer now joins memory and the - call log in the modulo set, since a static call still refills it. -- No unsupported-surface predicate changed. `stmtTouchesUnsupported*Surface` - keeps gating the call-family constructors exactly as before, so no headline - theorem gains coverage and behaviour is identical at the default empty buffer. - This is a soundness repair of the model, not a fragment widening. -- `Stmt.returndataCopy` stays conservative on both layers: `src + size = 0` - continues (it writes nothing whatever the buffer holds) and every other extent - is the EVM's exceptional halt, `.revert`. `SourceSemantics` and - `IRInterpreter` branch identically, so the two layers still agree on the nose. - Only the justifying comments needed correcting — "the buffer is empty in this - fragment" is now false, while "we conservatively under-approximate" is true. - **Superseded by *Bounded Returndatacopy (2026-09)* below: in-bounds extents - now perform the copy.** -- No new axiom and no new trust assumption. `Contracts.returndataSize` in the - executable EDSL surface remains the constant-zero stub, alongside its - `calldatasize` / `mload` / `extcodesize` placeholder siblings. - -## EDSL Executable Plane: Hashed Nested/Struct Mappings (2026-09) - -- `Contracts.getMappingN` / `setMappingN` / `getMappingWord` / - `setMappingWord` were `pure 0` / `pure ()` stubs: nested and struct - mappings had no executable state, so executable-plane theorems about - them were vacuous (#2416, Pareto G2). They now read and write the `.slot` - channel of `ContractState` at the Solidity keccak slot - (`Compiler.Proofs.abstractMappingSlot`, folded left over the key path; - struct/word offsets added modulo `2^256`), the same derivation the model - plane (`DenoteOracle.mappingSlot`) and the generated struct-mapping - accessors use. `transient` chains use the `.transient` channel. -- `structMembers` / `structMembers2` destructuring lowers to per-member - `structMember` reads in the executable plane. -- Hashed slots live in the `.slot` channel, disjoint from the - constructor-keyed `map` / `mapUint` / `map2` channels; distinctness of - slots under different keys uses the existing `solidityMappingSlot_injective` - axiom, adjacent struct words are distinct without it. -- Smoke: `Contracts/Smoke/HashedMappings.lean`. Zero new axioms. - -## EDSL Executable Plane: Getter Hops and Deferred Links (2026-09) - -- G23 (#2439): bound typed calls whose method names a public storage field of - the callee lower to `Contract.hopCallView target ()` - instead of applying the `StorageSlot` constant as a function (which failed - to elaborate). `Bool` getters decode the 0/1 word as `word != 0`. Packed, - transient and non-Uint256/Address field shapes fail closed at elaboration. -- G15 (#2415): `linked_contracts name : IFace := deferred` for cyclic - bindings. Typed calls on a deferred binding keep the ABI external-call - lowering but always use the threaded `ExecutableCallContext`; the context - binder is propagated through the existing helper fixed point. Fidelity is - by instantiation: `AdversaryModel.withViewLinks` answers selected static - sites by a linked body in a view hop, with the fidelity lemma - `Contracts.externalStaticCallContractWordsTo_withViewLinks` (and a revert - counterpart). With the stub context results are unchanged. -- Compilation-model lowering is unchanged for both. Smoke: - `Contracts/Smoke/LinkedGettersAndDeferred.lean`. Zero new axioms. -- G26: a bound typed call whose resolved callee constant takes the - `ExecutableCallContext` forwarded the caller's context, but a caller with no - reentrancy window of its own had the fixed `AdversaryModel.stub` as its - context, so the callee's nested deferred calls were answered by the stub. - Such a bound call now makes the enclosing function take the context - (`bodyNeedsLinkedCallContext`, same helper fixed point as G15). Smoke: - `Contracts/Smoke/HopContextAndMutableTuples.lean` (`wrap_stub` vs - `wrap_withViewLinks`, `viaHelper_withViewLinks`). -- G14 residual: typed interface tuple calls are no longer restricted to `view` - methods. Mutable tuple calls lower to `Contract.hopCall` (bound), the - mutable ABI external call with arity = number of results (unbound / - deferred), and `Compiler.Modules.Calls.withReturnsModule` in the - compilation model (new ECM `externalCallWithReturns`, classified - `abiBoundary`, assumption `external_call_abi_interface`). Smoke: - `run_bound_executes_callee`, `run_bound_revert_bubbles`, - `run_unbound_stub`, `run_deferred_stub`. -- G25 (documented rule, no code): named bindings dispatch by interface, not by - runtime target; an interface used on several different contracts must be - `deferred` and answered by `withViewLinks` keyed by target. - -## Bounded Returndatacopy (2026-09) - -- Supersedes the conservative `returndataCopy` posture recorded above. Once the - EIP-211 buffer is first-class (#2400), a `returndataCopy dst src size` whose - extent fits (`src + size ≤ 32 * buffer.length`) copies the buffer's bytes - into memory instead of reverting — complete words are replaced and, when - `size % 32 ≠ 0`, the final partial destination word is merged with its - untouched low bytes; only out-of-bounds extents - are observed as the EVM's exceptional halt, `.revert`. The semantics is - identical on all four lanes — `SourceSemantics.execStmt` and - `execStmtWithHelpers`, the compiler-free `Denote.execStmt`, and the IR - interpreter's `evalIRCall` / `evalIRCallWithInternals` — so the layers still - agree on the nose. -- The word model mirrors the `calldatacopy` lane - (`Compiler.Proofs.YulGeneration.Calldata`): `returndataloadWord` is the - byte-addressed, zero-extended read of the word list (no selector prefix), the - destination region is the shared `calldatacopyWritesAt dst size` word range, - and `returndatacopyMemoryPadded` is the IR-side memory update: the - complete-word kernel `returndatacopyMemory` plus, for `size % 32 ≠ 0`, the - ceiling-word merge that writes the high `size % 32` copied bytes while the - low bytes survive from memory — the returndata analogue of the - calldata lane's `calldatacopyMemoryPadded`. Every copied word is - below the EVM modulus (`returndataloadWord_lt_evmModulus`), so - `Uint256.ofNat` wrapping is the identity on the copied region. -- `runtimeStateMatchesIR_returndatacopyBothMemory` is the new reusable bridge: - when source and IR memories matched before the step, they match after the - copy, using the eighteenth `runtimeStateMatchesIR` conjunct to pin the - buffers equal. `IRStmtPreservesObsAt_of_returndatacopy` is generalized from - the zero-extent shape to every in-bounds extent. -- `compiledStmtStep_returndatacopy_empty_single` is reproved: the zero-extent - copy fits every buffer and still writes nothing, so the admitted fragment - behaves exactly as before (`returndatacopyMemory_zero`). New: - `compiledStmtStep_returndatacopy_bounded_single` proves the compiled single - `returndatacopy(destIR, srcIR, sizeIR)` step corresponds to - `Stmt.returndataCopy destOffset sourceOffset size` for *every* extent — both - layers branch on the same fit guard, so no side condition is needed. -- Partial-extent repair (review round on #2401): the bounded update first - shipped as the complete-word kernel alone, so an in-bounds copy with - `size % 32 ≠ 0` — a one-byte copy being the smallest case — left the final - destination word unchanged, i.e. the copy was modelled as a no-op there. All - four lanes now apply the padded update above, and - `returndataCopy_one_byte_merges_ceiling_word` kernel-checks the regression: - the copied high byte lands and the 31-byte destination suffix survives. - `compiledStmtStep_returndatacopy_bounded_single` stays exposed for every - extent — no word-alignment restriction was added and no previously supported - behaviour was reclassified. -- No unsupported-surface predicate changed: the generic proof fragment still - admits only `returndataCopyEmptySingle`, so no headline theorem gains - coverage. `Contracts.returndataCopy` in the executable EDSL remains a no-op - stub, next to the constant-zero `Contracts.returndataSize`. -- No new axiom and no new trust assumption. - -## Pure `exp` Builtin Lane (2026-08) - -- `pow a b` / `a ^ b` in the EDSL lowers to - `Expr.externalCall builtinExpName [base, exponent]`, a reserved sentinel that - the compiler already lowered to the pure Yul `exp` builtin rather than to a - foreign call. The node shape made it look like an external call to every - proof-side surface predicate, so the whole arm was gated as Tier-4 foreign - behaviour. That gate is now keyed on the sentinel: the eight - `exprTouchesUnsupported*Surface` / `exprTouchesInternalHelperSurface` - predicates recurse into `base`/`exponent` for `builtinExpName` at arity two - and keep their previous fail-closed constant for every other - `Expr.externalCall`. -- The lane is executed, not assumed, at all four planes: `SourceSemantics` - (`evalExpr`, `evalExprWithHelpers`), the compiler-free denotation - (`Denote.evalExpr`, with `DenoteAgreement` extended), the IR interpreter - (which routes `"exp"` to the EVMYulLean backend), and EVMYulLean itself. - All four denote `Uint256.pow`, i.e. `(a % 2^256) ^ (b % 2^256) % 2^256`. -- `evalPureBuiltinViaEvmYulLean_exp_native` closes the backend leg by proving - `EvmYul.UInt256.exp` equals that value, via `uint256_powAux_toNat` / - `uint256_pow_toNat` on the square-and-multiply loop. No `native_decide`. -- `ExprCompileCore.builtinExp` admits the lane to the generic compile core, so - `pow` may appear in generic-fragment expressions, `require` conditions, and - `emit` arguments. `compileExpr_builtinExp_ok` and - `eval_compileExpr_builtinExp_of_compiled` prove compilation and evaluation - agreement against `YulExpr.call "exp"`. -- `collectExprNames` no longer reports `builtinExpName` as a callee identifier. - The sentinel never reaches generated Yul (it becomes `exp`) and is already a - reserved name, so it cannot collide with a compiler-generated temp. This - matches `TrustSurface.collectExternalExprNames`, which has always skipped it, - and it restores `collectExprNames ⊆ exprBoundNames` on the compile core. -- No new axiom and no new trust assumption: this eliminates an unsupported - surface rather than assuming one, and it removes `pow` from the assumed - external-call bucket. Genuine `Expr.externalCall` targets, `Expr.call` / - `staticcall` / `delegatecall`, and `Stmt.externalCallBind` remain gated. - -## Audit Artifacts - -| Artifact | Purpose | Check | -|----------|---------|-------| -| `artifacts/evmyullean_native_lowering_report.json` | Native lowering coverage, admitted bridge lemmas, safe-body integration status | `python3 scripts/generate_evmyullean_native_lowering_report.py --check` | -| `artifacts/evmyullean_fork_audit.json` | Pinned fork divergence and non-semantic fork delta | `python3 scripts/generate_evmyullean_fork_audit.py --check` | -| `artifacts/evmyullean_capability_report.json` | EVMYulLean capability surface and reference-oracle paths | `python3 scripts/generate_evmyullean_capability_report.py --check` | -| `artifacts/storage_layout_report.json` + `artifacts/STORAGE_LAYOUT_SUMMARY.md` | Per-contract storage layout for migration/audit review: explicit slots, alias ranges, reserved ranges, packed subfields, mappings, dynamic arrays, opt-in namespaces (#1897) | `python3 scripts/generate_storage_layout_report.py --check --no-lean` (drift gate in `make check`); regenerate with `make regen-storage-layout-report` | -| `PrintAxioms.lean` / generated axiom report | Axiom dependency visibility | `python3 scripts/generate_print_axioms.py --check` and `lake build PrintAxioms` | -| `Compiler.Proofs.IRGeneration.IntrinsicProofs` | Proven Verity-owned intrinsic plumbing: scope accounting, generic lowering shape, fork-order facts, and arity rejection | `lake build Compiler.Proofs.IRGeneration.IntrinsicProofs` | -| Intrinsic fork gate | Fail-closed `min_fork` enforcement against `--target-fork` / `YulEmitOptions.targetFork` | `lake build Compiler.CompileDriverTest` | -| `trust_report.intrinsics[*]` | Planned consumer-declared intrinsic trust surface: name, emission mode, opcode/builtin target, obligation, `min_fork`, and source location | Follow-up hardening; until then, grep consumer trees for `verity_intrinsic` | - -## Storage-Lens API Freeze — C5 Step 1 (2026-08) - -- `scripts/check_storage_lens_freeze.py` (in `make check`) ratchets raw - `ContractState` storage-channel record updates: new - `{ s with storageMap := ... }`-style sites fail CI; the 304 existing sites - are frozen per file in the script's `BASELINE` and must only shrink. - Exactly-lens-shaped helpers (`setLock`, `ReentrancyExample.setStorageSlot`/ - `setMappingSlot`, adversary transitions in the call-program examples, - `TypedIRCompilerCorrectness` typed-IR write helpers) are already migrated - to `writeSlot`/`writeMap`/`writeTransient`. -- This is step 1 of the C5 storage-representation flip (single word-addressed - map + Solidity-layout slot derivation under stable lens names); steps 2–4 - (proofs onto `storage_simps`, representation flip, compiler slot - correspondence) are tracked in `docs/ROADMAP.md`. No semantic change: the - lenses are definitionally the former raw updates. - -## Storage-Lens Simp Flip — C5 Step 2 (2026-08) - -- The ContractState lenses are no longer default-`simp` transparent: the - global `attribute [simp]` block in `Verity/Core.lean` is removed. - `storage_simps` stays laws-only (read-over-write normalization; it - deliberately does not unfold lenses to the raw record representation, so it - survives the step-3 flip). Proofs that genuinely need the current raw - representation now name the lens explicitly in their `simp` lists - (`simp [ContractState.writeSlot]`) — those call sites are the step-3 - burn-down inventory, greppable as `ContractState.read`/`ContractState.write` - occurrences inside simp lists. -- Repair surface: ~25 files (Core storage-array run-lemmas, Stdlib - Automation/MappingAutomation, NonReentrantGuard, the `denote_stmt_arm` - macro in `DenoteAgreement`, the `simp_tir_eval` macro in - `TypedIRCompilerCorrectness`, and the contract proof suites). No theorem - statement changed; no semantic change; zero axioms. - -## StorageKey Canonical Backing — C5 Step 3 (2026-08) - -- `ContractState` now stores word-valued channels in one - `storageWords : StorageKey → Uint256` map. `StorageKey` is an injective - inductive (`slot` / `contractSlot` / `transient` / `addr` / `map` / - `mapUint` / `map2`). Solidity keccak slot derivation stays on the - compiler side; lens laws use constructor injectivity, not hash - injectivity. -- Public accessors keep the old names (`storage`, `storageAddr`, - `storageMap`, …). Specs that read those views do not change shape. - `storageArray` and `knownAddresses` remain separate fields this step; - independence from the word-channel lenses is now proved - (`Compiler.Proofs.Storage.SeparateChannels`). -- Four-feature increment (not a C5 fold): `FieldType.mappingFixedArray` - compiles element reads/writes at `mappingSlot(slot, key) + i`; - CodeData/SSTORE2 layout theorems pin the `STOP` prefix and offset-1 - read; transient `fixedArrayUint128` uses `tload`/`tstore` and - `fieldRootKey` maps it to `StorageKey.transient` (`storageKeySlot = - none`); `denoteSelfDelegateCalls` is a first-class self-delegate - sequence (not the ECM, not #2365's compiled `FunctionSpec` path). - Zero new axioms. -- The freeze baseline shrinks to `Verity/Core.lean` (lens - implementations) plus `Contracts/TypedIRTests.lean` (IRState field-name - collisions). `storageWords :=` is forbidden outside `Verity/Core.lean`. -- C5 step 4 complete under `solidityMappingSlot_injective` - (not axiom-free): `Compiler.Proofs.Storage.MappingCoherence` - now defines `storageKeySlot` and address-keyed `MappingCoherent`, - with `defaultState_mappingCoherent` / `MappingCoherentUint` / - `MappingCoherentMap2` and the aligned `writeMap`/`writeMapUint`/ - `writeMap2`+`writeSlot` laws (same pair, plus other pairs under an - explicit non-alias hypothesis). `FieldStorageKey` collapses a - CompilationModel field list to a root `StorageKey` (and typed - `map` / `mapUint` / `map2` entries) and proves `storageKeySlot` - of that key is the resolved slot. Finite-set address - `MappingCoherentOn` is preserved by an aligned write under an - explicit pairwise derived-slot certificate - (`MappingCoherenceOn`), including uint and nested-address lists. - Address-keyed `mappingStruct` / `mappingStruct2` members collapse - to `mappingSlotLocation` / `nestedMappingSlotLocation`. Compatibility - `aliasSlots` are extra compiler write targets; the resolved head is - the field's `storageKeySlot`. Cross-channel finite-set preservation - (aligned write on one mapping channel vs another channel's list) - takes an explicit derived-slot inequality. Packed subfields extract - from the `storageKeySlot` word (`packedExtract`); compiler packed-read - composition with SolidityStorage is `fieldPackedExtract_eq_compiledPackedRead` - (`width < 256`). `FieldCoherence` identifies a named mapping field's - `storageKeySlot` with the flat slot `MappingCoherent*` / - `MappingCoherentOn*` reads. - A lone `writeSlot` preserves a finite `*On` list under an explicit - listed-vs-written slot inequality. A lone `writeTransient` preserves - the same lists by constructor injectivity (`StorageKey.transient` - vs persistent `.slot` / `.map` / `.mapUint` / `.map2`); no slot - inequality. Global aligned `writeMap` / `writeMapUint` / - `writeMap2`+`writeSlot` preserves `MappingCoherent*`. - `writeTransient` preserves the globals by constructor injectivity. - A lone `writeSlot` preserves a global under an explicit - image-avoidance `∀` (derived slot ≠ written word). That `∀` is - discharged for writes that never touch `StorageKey.slot`: - `writeTransient` (constructor injectivity), `writeAddrSlot` - (constructor injectivity of `.addr`), and `writeArray` (separate - `storageArray` field). Finite `*On` lists lift from the globals - without a pairwise certificate. Arbitrary `n` is still not claimed - safe for a lone `writeSlot`. - `FieldEncode` derives `findResolvedFieldAtSlot` from - `findFieldWithResolvedSlot` plus no write-slot conflict, persistent, - and unpacked, then identifies that slot with `encodeStorageAt`. - An unoccupied mapping-derived slot (address, uint, or map2) encodes - as the `MappingCoherent*` / `MappingCoherentOn*` shadow. bytes32-keyed - maps collapse to `solidityMappingSlot` of the 32-byte word (no - `StorageKey.mapBytes32`). All nine `MappingType.nested` key-type - pairs collapse to `abstractNestedMappingSlot`. -- C5 step 4 final slice (`Compiler.Proofs.Storage.MappingCoherentGlobal`, - #2330): `storageKeySlot : List Field → StorageKey → Option (Channel × Nat)` - is the field-list collapse — `Channel` is `persistent` / `address` / - `transient` / `contract c`, and `contractSlot 0` is **not** identified - with `slot`. `MappingCoherentAllKeys` is the all-keys (`∀ StorageKey`) - invariant, not a listed pair or a single key. The declared layout is - load-bearing: a mapping key collapses only when the resolved field at its - base slot declares the matching `MappingType`, so distinct mapping shapes - force distinct base slots and `solidityMappingSlot_injective` separates - their derived images. Aligned `writeMap` / `writeMapUint` / `writeMap2` + - `writeSlot`, `writeAddrSlot`, and `writeTransient` preserve it; a lone - `writeSlot` takes `DerivedMappingSlotsAvoid` (the existing image-avoidance - `∀`); the simple-vs-nested cross case takes the layout certificate - `MappingBasesNotDerived`. `readMap_eq_encodeStorageAt_of_coherent` (plus - `mapUint` / `map2` variants) is the lens-read = flat-read bridge under - that invariant. Dynamic-array element slots are an explicit - `unsupported` row, not an equivalence: the source model derives element - `i` at `solidityMappingSlot s i` - (`Compiler/Proofs/IRGeneration/SourceSemantics.lean:330-350`) while the - Yul layout uses `keccak256(bytes32(s)) + i` - (`Compiler/Proofs/Storage/StructArrayStorage.lean:666-671`); the collapse - returns `none` at dynamic-array roots - (`storageKeySlot_slot_dynamicArray`). -- Zero new axioms. - -## Hop Storage Namespacing — G24 (#2440, 2026-09) - -- `StorageKey` gains `scoped (contract : Nat) (key : StorageKey)`: the parked - copy of a non-slot channel key (`addr` / `transient` / `map` / `mapUint` / - `map2`, see `StorageKey.isPlainNonSlot`) of contract `contract`. -- `ContractState.switchSlotWorld parkId loadId` (used by `enterHop` / - `exitHop`, hence `Contract.hopCall` / `Contract.hopCallView`) now namespaces - every word-valued channel: `.slot n` loads `.contractSlot loadId n` (as - before), each plain non-slot key `k` loads `.scoped loadId k`, and the - current plain world is parked under `.contractSlot parkId` / `.scoped parkId`. - Previously address slots, transient slots and mappings were shared between - caller and callee across a hop, which made cross-contract fidelity - statements over address fields or mappings unsound. -- New lemmas (`Verity/Core.lean`): `enterHop_readAddrSlot`, - `enterHop_readTransient`, `enterHop_readMap`, `enterHop_readMapUint`, - `enterHop_readMap2`, `enterHop_storageWords_plainNonSlot`, - `enterHop_scoped_caller`, `exitHop_storageWords_plainNonSlot`, - `exitHop_scoped_callee`, `exitHop_contractSlot_callee`, - `switchSlotWorld_{plainNonSlot,scoped_park,scoped_other,contractSlot_park,contractSlot_other}`, - and the round trip `exitHop_enterHop_storageWords_plain`. - Smoke: `Contracts/Smoke/HopNamespace.lean` (callee address slot / mapping - are its own, caller untouched, second hop sees the parked write, nested - A→B→A reentrant hop sees A's own world, revert restores). -- Compiler storage-coherence collapses map `scoped` keys to `none` - (`MappingCoherence.storageKeySlot`, `MappingCoherentAllKeys.storageKeySlot`): - they have no flat compiler-channel counterpart. -- Remaining boundary: `storageArray` (dynamic arrays, a separate field) is - still global across hops. -- Zero new axioms. - -## CI Guards - -- `make check` validates generated reports, bridge coverage synchronization, - builtin bridge matrix synchronization, Lean hygiene, proof length, and - documentation counters. -- `make test-evmyullean-fork` validates the pinned fork audit, checks the - native lowering report, rebuilds the public EndToEnd EVMYulLean target, and - runs the concrete bridge-equivalence tests. -- `.github/workflows/evmyullean-fork-conformance.yml` runs the EVMYulLean fork - conformance probe weekly. Scheduled or manual failures fail the workflow and - open or update a GitHub issue for drift triage. -- Intrinsic fork enforcement is fail-closed: builds using an intrinsic whose - `min_fork` exceeds the contract target fail unless the caller passes - `--allow-future-fork-intrinsics`. - -## Update Checklist - -1. Update `TRUST_ASSUMPTIONS.md` for the human-readable trust boundary. -2. Update `AXIOMS.md` if axioms, non-axiom trusted primitives, or soundness - controls changed. -3. Update this file with the changed audit state, artifacts, and CI guards. -4. Regenerate deterministic artifacts with the relevant `scripts/generate_*.py` - command. -5. Run `make check`; run targeted Lean builds for changed proof modules. - -**Last Updated**: 2026-06 (cross-function reentrancy gate: fail-closed `validateReentrancyDisposition` rejection of external-call functions lacking a sound disposition, new `reentrancy_trusted` metadata annotation, CEISafety demoted to single-function ordering hygiene, `#guard_msgs` regression pair in `SecurityCombos.lean` — axiom-free; reentrancy rely-guarantee framework: `Verity.Core.Invariant` / `Verity.Core.Reentrancy`, `Env.reenter` hook, Midnight `take`/`liquidate` worked example — additive, 0 axioms; event emission proof-model alignment, emit-argument scope collection, scratch memory wrapping; storage layout audit artifacts, #1897; nonreentrant transient-storage guard, #1893; non-alias certificate write-set overlap, #1967; nonreentrant fork requirement, #1968) diff --git a/CLAUDE.md b/CLAUDE.md index cc37459ff0..508b65774b 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -1,10 +1,8 @@ # Agent Guide for Verity -This file is the operator quickstart. Keep it short and enforceable. - ## Non-Negotiables -1. Keep `AUDIT.md`, `TRUST_ASSUMPTIONS.md`, and `AXIOMS.md` synchronized with any semantic/trust/CI boundary change. +1. Keep `docs/TRUST_ASSUMPTIONS.md` and `docs/AXIOMS.md` synchronized with any semantic, trust or CI boundary change. 2. Never claim completion without evidence and passing checks. ## Core Commands @@ -18,7 +16,7 @@ make test-foundry # Run Foundry differential tests ## Reference Docs -- Project overview: [README.md](README.md) +- Project overview and review order: [README.md](README.md) - Contribution conventions: [CONTRIBUTING.md](CONTRIBUTING.md) - Roadmap: [docs/ROADMAP.md](docs/ROADMAP.md) -- Script reference: [scripts/REFERENCE.md](scripts/REFERENCE.md) +- Solidity import: [docs/SOLIDITY_IMPORT.md](docs/SOLIDITY_IMPORT.md) diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index de714d91ab..78476f19c5 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -1,308 +1,57 @@ # Contributing to Verity -Quick conventions for contributing. See [README.md](README.md) for project overview, [docs/ROADMAP.md](docs/ROADMAP.md) for status. +## Pull requests -## Issue Format +Title with a category prefix (`[Layer 3]`, `[Trust Reduction]`, +`[Compiler Enhancement]`, `[Documentation]`, `[Infrastructure]`, …). The body +has a **Summary**, a **Test Plan** (what you ran), and **Related Issues**. +Issues use the templates in `.github/ISSUE_TEMPLATE/`. -**Title**: `[Category] Brief description` +Before opening a PR: -**Categories**: `[Layer 3]` `[Trust Reduction]` `[Property Coverage]` `[Compiler Enhancement]` `[Documentation]` `[Infrastructure]` - -**Labels**: Use `layer-3` `lean` `proof` `enhancement` `bug` `documentation` `help-wanted` `good-first-issue` - -**Structure**: Use issue templates in `.github/ISSUE_TEMPLATE/` -- Goal (what needs doing) -- Effort estimate (hours/weeks) -- Implementation approach -- Acceptance criteria -- References (file paths, issues) - -## PR Format - -**Title**: Same `[Category]` prefix as issues - -**Body**: -```markdown -## Summary -- Bullet points of changes - -## Test Plan -How you verified it works - -## Related Issues -Closes #X, addresses #Y -``` - -**Before submitting**: -```bash -lake build # Must pass — verifies all proofs -FOUNDRY_PROFILE=difftest forge test # Must pass — runs all Foundry tests -# FOUNDRY_PROFILE=difftest is required because property and differential -# tests use vm.ffi() to invoke the Lean-based interpreter -# No new `sorry` without documentation -# No new `axiom` without documenting in AXIOMS.md -# Update docs/ROADMAP.md if architectural changes -# If adding a new contract, run the checklist below -``` - -**When adding a new contract**, also update: -- Author the contract with `verity_contract`; do not add a separate hand-written `Compiler.Specs` body unless the workflow truly requires a special case like external linking. -- A new reusable facet (Ownable, Pausable, …) is a `verity_mixin` plus named-slot specs, a `Footprint`, and `_meets_spec` / guard theorems. Hosts `include` the mixin; do not copy `Owned.lean` into the host. -- Add the macro-generated `.spec` alias/list entry in [`Contracts/Specs.lean`](Contracts/Specs.lean) if the contract should ship through the default compiler manifest. -- `test/property_manifest.json` — Run `python3 scripts/extract_property_manifest.py` -- `README.md` — Contracts table (theorem count and description) -- `docs/VERIFICATION_STATUS.md` — Contract table and coverage stats -- `docs-site/public/llms.txt` — Quick Facts and theorem breakdown table -- `docs-site/content/verification.mdx` — Snapshot section -- `docs-site/content/examples.mdx` — Contract descriptions and count -- Plus any other files that reference theorem/test/contract counts (e.g., `compiler.mdx`, `research.mdx`, `index.mdx`, `layout.tsx`, `ROADMAP.md`, `TRUST_ASSUMPTIONS.md`, `test/README.md`) -- Run `python3 scripts/check_contract_structure.py` to verify file structure is complete -- Run `python3 scripts/generate_verification_status.py --check` to verify the machine-readable status artifact is current -- Run `python3 scripts/check_lean_hygiene.py` to verify no `#eval` in proof files and `allowUnsafeReducibility` count is correct - -## Adding an Intrinsic - -Declare consumer-owned opcode bindings with `verity_intrinsic` only when the -consumer owns the temporary trust boundary. Keep Verity opcode-agnostic: changes -in this repository should add generic intrinsic mechanics, not opcode-specific -business logic. The consumer repository must document any generated obligation -in its own `AXIOMS.md` or trust-boundary document. Set `min_fork` to the first -fork where the emitted opcode is valid; Verity accepts `cancun`, `prague`, and -`osaka` (`fusaka` is parsed as the Ethereum combined-upgrade alias) and enforces -the declaration against `--target-fork`. - -See [docs/INTRINSICS.md](docs/INTRINSICS.md) for the declaration format, -audit checklist, and worked example. - -## Proof Hygiene Requirements - -Every PR that touches proof files must satisfy all of the following. -Each requirement is enforced by CI and cannot be bypassed without updating -the corresponding enforcement script. - -1. **Zero `sorry`**: `lake build` rejects incomplete proofs at the Lean kernel level. - CI runs an independent grep scan as defense in depth - ([`check_lean_hygiene.py`](scripts/check_lean_hygiene.py)). - -2. **Zero new axioms** without a same-commit update to [AXIOMS.md](AXIOMS.md) - including: justification, risk rating, CI validation strategy, and - elimination path. Enforced by - [`check_axioms.py`](scripts/check_axioms.py). - -3. **Axiom dependency freshness**: If theorems are added or removed, - `PrintAxioms.lean` must be regenerated - (`python3 scripts/generate_print_axioms.py`). - CI validates via `--check` mode. - -4. **Proof size**: Proofs should be under 30 lines. Proofs over 50 lines - require an entry in the allowlist in - [`check_proof_length.py`](scripts/check_proof_length.py) with a - PR comment explaining why decomposition is not feasible. Enforced by CI. - -5. **No `native_decide` in contract proofs**: `native_decide` bypasses the - Lean kernel checker. It is allowed only in smoke tests - (`Compiler/Proofs/YulGeneration/SmokeTests.lean`). Enforced by - [`check_lean_hygiene.py`](scripts/check_lean_hygiene.py). - -6. **No debug commands in proofs**: `#eval`, `#check`, `#print`, `#reduce` are - forbidden in proof files (they slow incremental builds). Enforced by - [`check_lean_hygiene.py`](scripts/check_lean_hygiene.py). - -7. **Verification status freshness**: Regenerate and commit - [`artifacts/verification_status.json`](artifacts/verification_status.json) - when theorem/test/axiom counts change. - Enforced by [`generate_verification_status.py`](scripts/generate_verification_status.py). - -## Code Style - -**Lean**: -- 2-space indentation -- Meaningful names (`irState` not `s`) -- Proofs under 30 lines when possible (see Proof Hygiene above) -- Document complex proof strategies - -**Commits**: -``` -type: description - -[optional body] - -Co-Authored-By: Claude -``` - -Types: `feat` `fix` `proof` `docs` `refactor` `test` `chore` - -## Code Comment Conventions - -Follow these conventions to keep documentation accurate and maintainable. - -### Proof Status - -**For Theorems**: -- `sorry` - Incomplete proof (blocked by CI in proof files) -- No marker - Complete proof -- **DON'T** use TODO/STUB in proven theorems - -```lean --- ✅ GOOD: Complete proof, no marker needed -theorem my_theorem : ... := by - unfold ... - simp [...] - --- ❌ BAD: Complete proof but has misleading TODO -theorem my_theorem : ... := by - -- TODO: This actually works fine - unfold ... - simp [...] -``` - -**For Incomplete Proofs**: -```lean --- Only in draft branches, NOT in main -theorem draft_theorem : ... := by - sorry -- Strategy: Use induction on List structure - -- See issue #123 for details +```sh +lake build # every proof must check +make check # repository checks, as in CI +make test-foundry # property and differential tests (uses vm.ffi) ``` -### Module Documentation - -**Module Headers** should reflect current status: - -```lean -/-! ## Module Name - -Brief description of what this module does. - -**Status**: Complete | Partial | Draft -**Dependencies**: List axioms/external dependencies -**Related**: Links to other relevant modules --/ -``` - -**Status Definitions**: -- **Complete**: All theorems proven, no sorry -- **Partial**: Some proven, some sorry (specify which) -- **Draft**: Structural outline only - -**Example**: -```lean -/-! ## Layer 3: Statement-Level Equivalence - -Proves IR statement execution is equivalent to Yul statement execution -when states are aligned. - -**Status**: Complete - All statement types proven -**Dependencies**: Uses keccak256 axiom (see AXIOMS.md) --/ -``` - -### Future Work Comments - -For planned improvements or known limitations: - -```lean --- FUTURE: Add support for delegatecall --- See issue #123 for design discussion -def currentImplementation := ... -``` - -**DON'T** use: -- ~~`TODO:`~~ (implies incomplete current work) -- ~~`FIXME:`~~ (implies broken code) -- ~~`HACK:`~~ (use `NOTE:` with explanation instead) - -### Implementation Notes - -For non-obvious design choices: - -```lean --- NOTE: We use fuel-based recursion here because Lean cannot prove --- termination for mutually recursive functions with this structure. --- See Equivalence.lean for the proof strategy this enables. -def execIRStmtFuel (fuel : Nat) : ... := ... -``` - -### Axiom Documentation - -All axioms **must** be documented inline **and** in AXIOMS.md: - -```lean -/-- AXIOM: explain the remaining trust boundary - -Document why the declaration cannot yet be proved in Lean, -what guards or regression checks keep it honest, -and link to AXIOMS.md for the current trust story. -See AXIOMS.md for full soundness justification. --/ -axiom some_remaining_axiom : Prop -``` - -### Property Test Tags - -Property tests must match Lean theorem names exactly: - -```solidity -/// Property: store_retrieve_roundtrip -function testProp_StoreRetrieveRoundtrip(...) public { ... } -``` - -Tag format: `/// Property: exact_theorem_name` - -### What NOT to Include - -❌ **Stale TODOs** in completed code -❌ **Difficulty estimates** after proof is done -❌ **Verbose explanations** of obvious code -❌ **Status claims** that don't match reality - -✅ **Brief descriptions** of what code does -✅ **Links** to related issues for context -✅ **Explanations** of non-obvious design choices -✅ **Accurate status** in module headers - -## Development - -**Layer 3 proofs**: Read [Compiler/Proofs/README.md](Compiler/Proofs/README.md), study completed proofs (`assign_equiv`, `storageStore_equiv`), use templates - -**New contracts**: Use `python3 scripts/generate_contract.py ` to scaffold all boilerplate files, then follow the `SimpleStorage` pattern: Spec → Invariants → Implementation → Proofs → Tests - -**Compiler changes**: Ensure `lake build` and `make check` pass - -## Adding Axioms - -Axioms should be **avoided whenever possible** as they introduce trust assumptions. If you must add an axiom: +A change to the trust boundary, an axiom, or CI updates +[docs/TRUST_ASSUMPTIONS.md](docs/TRUST_ASSUMPTIONS.md) and +[docs/AXIOMS.md](docs/AXIOMS.md) in the same PR. -1. **Exhaust all alternatives first**: - - Can you prove it? (even if difficult) - - Can you use a weaker lemma? - - Can you refactor to avoid the need? +## Proof hygiene -2. **Document in AXIOMS.md**: - - State the axiom clearly - - Provide soundness justification - - Explain why a proof isn't feasible - - Note future work to eliminate axiom - - Assess risk level +Enforced by CI; changing a rule means changing its script. -3. **Add inline comment in source**: - ```lean - /-- - AXIOM: Brief explanation - See AXIOMS.md for full soundness justification - -/ - axiom my_axiom : ... - ``` +1. No `sorry` (`scripts/check_lean_hygiene.py` double-checks the build). +2. No new axiom without a same-commit entry in docs/AXIOMS.md + (`scripts/check_axioms.py`). +3. Regenerate `PrintAxioms.lean` (`scripts/generate_print_axioms.py`) and + `artifacts/verification_status.json` (`make refresh-status`) when theorems + or counts change. +4. Proofs stay under 30 lines; over 50 needs an allowlist entry with a reason + (`scripts/check_proof_length.py`). +5. No `native_decide` outside smoke tests, and no `#eval`/`#check`/`#print` + in proof files. -4. **CI will validate**: The CI workflow automatically detects axioms and ensures they're documented in AXIOMS.md. Undocumented axioms will fail the build. +## Adding an axiom -See [AXIOMS.md](AXIOMS.md) for current axioms and detailed guidelines. +Avoid it: try to prove it, weaken the lemma, or refactor. If it is truly +needed, document it in docs/AXIOMS.md (statement, soundness justification, why +a proof is not feasible, risk, elimination path) and mark the declaration with +an `AXIOM:` doc comment pointing there. -## Key Files +## Adding a contract or intrinsic -- `Verity/` - EDSL, user-facing specs, and contract-specific specification proofs -- `Compiler/` - IR, Yul, codegen -- `Compiler/Proofs/` - Layer 2 & 3 proofs -- `docs/ROADMAP.md` - Progress tracking -- `test/` - Foundry tests +- Write contracts with `verity_contract`; reusable facets are `verity_mixin`s + that hosts `include`. `python3 scripts/generate_contract.py ` + scaffolds the files; `scripts/check_contract_structure.py` checks them. +- Refresh `test/property_manifest.json` + (`python3 scripts/extract_property_manifest.py`) and the counts + (`make refresh-status`, `python3 scripts/update_doc_numbers.py`). +- Intrinsics (`verity_intrinsic`) are consumer-owned opcode bindings; see + [docs/INTRINSICS.md](docs/INTRINSICS.md). +- Solidity imports: see [docs/SOLIDITY_IMPORT.md](docs/SOLIDITY_IMPORT.md) for + what a new supported construct must ship with. -Full details: [docs/VERIFICATION_STATUS.md](docs/VERIFICATION_STATUS.md) +Layer 3 proof work starts from [Compiler/Proofs/README.md](Compiler/Proofs/README.md). diff --git a/Compiler/CompileDriverCommon.lean b/Compiler/CompileDriverCommon.lean index a7893dcb84..97863bc792 100644 --- a/Compiler/CompileDriverCommon.lean +++ b/Compiler/CompileDriverCommon.lean @@ -675,7 +675,7 @@ def compileSpecsWithOptions IO.println s!" {spec.name}: {String.intercalate ", " primitives}" if !anyAxiomatized then IO.println " (no axiomatized primitives used)" - IO.println " Proof boundary: these primitives compile through explicit trusted boundaries (for example, keccak-backed hashing) and should be audited alongside AXIOMS.md/TRUST_ASSUMPTIONS.md." + IO.println " Proof boundary: these primitives compile through explicit trusted boundaries (for example, keccak-backed hashing) and should be audited alongside docs/AXIOMS.md and docs/TRUST_ASSUMPTIONS.md." IO.println "" IO.println "Local obligation report:" let mut anyLocalObligations := false diff --git a/Compiler/Linker.lean b/Compiler/Linker.lean index f420c4fcf9..c2e110731c 100644 --- a/Compiler/Linker.lean +++ b/Compiler/Linker.lean @@ -177,7 +177,7 @@ def loadLibrary (path : String) : IO (List LibraryFunction) := do **Note**: Library functions are injected at text level, not AST level. This is a known limitation (see issue #173). The formal proofs do not cover linked library code - this is a trust assumption documented in -`TRUST_ASSUMPTIONS.md`. +`docs/TRUST_ASSUMPTIONS.md`. -/ -- Indentation level for library functions in runtime code (3 levels × 4 spaces = 12 spaces) diff --git a/Compiler/Proofs/ArithmeticProfile.lean b/Compiler/Proofs/ArithmeticProfile.lean index 0577fe428c..7bd01ff179 100644 --- a/Compiler/Proofs/ArithmeticProfile.lean +++ b/Compiler/Proofs/ArithmeticProfile.lean @@ -163,7 +163,7 @@ theorem shr_bridge (shift value : Nat) : -- overflow checks. Contracts that need checked arithmetic must use the EDSL -- safeAdd/safeSub/safeMul functions, which return Option and revert on None. -- --- Cryptographic primitives: keccak256 is axiomatized (see AXIOMS.md). +-- Cryptographic primitives: keccak256 is axiomatized (see docs/AXIOMS.md). -- The mapping-slot derivation trusts the keccak FFI. -- -- Legacy/native bridge equivalence: comparison lemmas are part of the diff --git a/Compiler/Proofs/IRGeneration/Contract.lean b/Compiler/Proofs/IRGeneration/Contract.lean index dc61af6d21..81caeb5792 100644 --- a/Compiler/Proofs/IRGeneration/Contract.lean +++ b/Compiler/Proofs/IRGeneration/Contract.lean @@ -2046,7 +2046,7 @@ theorem compileFunctionSpec_correct_generic_with_helper_proofs_and_helper_ir_of_ /-- Primary whole-contract Layer 2 theorem: compilation preserves semantics for any supported `CompilationModel`. No contract-specific bridge premise. Layer 2 itself is axiom-free; the remaining documented project axiom is the -mapping-slot range assumption tracked in `AXIOMS.md`. -/ +mapping-slot range assumption tracked in `docs/AXIOMS.md`. -/ theorem compile_preserves_semantics (model : CompilationModel) (selectors : List Nat) diff --git a/Compiler/Proofs/IRGeneration/IRStorageWord.lean b/Compiler/Proofs/IRGeneration/IRStorageWord.lean index d5fb6caa1e..541525002a 100644 --- a/Compiler/Proofs/IRGeneration/IRStorageWord.lean +++ b/Compiler/Proofs/IRGeneration/IRStorageWord.lean @@ -1,8 +1,8 @@ /- IRStorageWord: typed-storage helper surface for the IR storage carrier. - This module is **Phase 0** of the IR storage type refactor described in - `docs/IR_STORAGE_UINT256_REFACTOR.md`. It introduces a type alias plus + This module is **Phase 0** of the IR storage type refactor. It introduces a + type alias plus canonical injection / projection helpers and round-trip lemmas, *without* changing the underlying carrier or any existing callsite. @@ -22,9 +22,6 @@ `simpleStorageNativeStoreHitBridge` by reduction through the bounded carrier, then drop the corresponding hypotheses from `simpleStorage_endToEnd_native_evmYulLean`. - - The full plan and acceptance signals live in - `docs/IR_STORAGE_UINT256_REFACTOR.md`. -/ import EvmYul.UInt256 diff --git a/Compiler/Proofs/IRGeneration/SupportedSpec.lean b/Compiler/Proofs/IRGeneration/SupportedSpec.lean index 25c072e7ec..360439408b 100644 --- a/Compiler/Proofs/IRGeneration/SupportedSpec.lean +++ b/Compiler/Proofs/IRGeneration/SupportedSpec.lean @@ -3240,7 +3240,7 @@ structure SupportedFunction (spec : CompilationModel) (fn : FunctionSpec) where /-- `nonreentrant(lockField)` guards sit outside the proven fragment: the TLOAD/TSTORE prologue injected by `attachNonReentrantGuard` is not yet modelled by the source semantics. This makes the documented boundary - (TRUST_ASSUMPTIONS.md) machine-checked instead of prose-only. -/ + (docs/TRUST_ASSUMPTIONS.md) machine-checked instead of prose-only. -/ noNonReentrant : fn.nonReentrantLock = none params : SupportedParamProfile fn.params returns : SupportedReturnProfile fn diff --git a/Compiler/Proofs/MappingSlot.lean b/Compiler/Proofs/MappingSlot.lean index 935f3a3c55..6eda5180e7 100644 --- a/Compiler/Proofs/MappingSlot.lean +++ b/Compiler/Proofs/MappingSlot.lean @@ -61,7 +61,7 @@ def solidityMappingSlot (baseSlot key : Nat) : Nat := `keccak256(abi.encode(key, baseSlot))`. **Not** injectivity of keccak256 on arbitrary `ByteArray`s (256-bit - output, infinite domain). See `AXIOMS.md`. -/ + output, infinite domain). See `docs/AXIOMS.md`. -/ axiom solidityMappingSlot_injective (base₁ key₁ base₂ key₂ : Nat) : solidityMappingSlot base₁ key₁ = solidityMappingSlot base₂ key₂ → diff --git a/Compiler/Proofs/README.md b/Compiler/Proofs/README.md index 7771589ee2..cf47f0bf6f 100644 --- a/Compiler/Proofs/README.md +++ b/Compiler/Proofs/README.md @@ -3,7 +3,7 @@ Formal verification proofs for the Verity compiler pipeline: `EDSL -> CompilationModel -> IR -> native EVMYulLean`. -See [TRUST_ASSUMPTIONS.md](../../TRUST_ASSUMPTIONS.md) for the full trust boundary and [veritylang.com/verification](https://veritylang.com/verification) for proof status. +See [TRUST_ASSUMPTIONS.md](../../docs/TRUST_ASSUMPTIONS.md) for the full trust boundary and [veritylang.com/verification](https://veritylang.com/verification) for proof status. ## Verification Layers diff --git a/Compiler/Proofs/Storage/MappingCoherentAllKeys.lean b/Compiler/Proofs/Storage/MappingCoherentAllKeys.lean index 0c4ee75789..deae41a11b 100644 --- a/Compiler/Proofs/Storage/MappingCoherentAllKeys.lean +++ b/Compiler/Proofs/Storage/MappingCoherentAllKeys.lean @@ -21,7 +21,7 @@ Trust: the aligned `writeMap*` laws inherit `solidityMappingSlot_injective` (ABI mapping-preimage collision resistance), exactly as the per-channel globals in `MappingCoherence` - already do — see AXIOMS.md. No new axiom is introduced here. The + already do — see docs/AXIOMS.md. No new axiom is introduced here. The map-vs-nested-map cross case additionally takes an explicit `MappingBasesNotDerived` layout certificate; it is a hypothesis of the same shape as the image-avoidance `∀` that diff --git a/Compiler/Selectors.lean b/Compiler/Selectors.lean index 8f01a0f8b9..37e0fcf967 100644 --- a/Compiler/Selectors.lean +++ b/Compiler/Selectors.lean @@ -16,7 +16,7 @@ defense-in-depth consistency check. ## References - Solidity ABI Spec: https://docs.soliditylang.org/en/latest/abi-spec.html#function-selector -- Trust Assumptions: TRUST_ASSUMPTIONS.md +- Trust Assumptions: docs/TRUST_ASSUMPTIONS.md -/ namespace Compiler diff --git a/Dockerfile b/Dockerfile deleted file mode 100644 index d718b5c9a2..0000000000 --- a/Dockerfile +++ /dev/null @@ -1,10 +0,0 @@ -FROM debian:latest -SHELL ["/bin/bash", "-c"] -RUN apt-get update && apt-get -y install build-essential git curl sudo python3 -RUN useradd -ms /bin/bash verity && \ - echo '%sudo ALL=(ALL) NOPASSWD:ALL' >> /etc/sudoers && \ - usermod -aG sudo verity -USER verity -WORKDIR /home/verity -COPY --chown=verity:verity . . -RUN make setup && source ~/.elan/env && make verify diff --git a/README.md b/README.md index 8fa5d83c3d..e10055463f 100644 --- a/README.md +++ b/README.md @@ -1,108 +1,46 @@ -

- Verity -

+# Verity -

Verity

+A formally verified smart contract compiler for Ethereum, written in +[Lean 4](https://lean-lang.org/). Write a contract in an embedded DSL (or +import it from Solidity), prove properties about it, and compile it to EVM +bytecode through a compiler whose layers are themselves proved. +Documentation: [veritylang.com](https://veritylang.com) · Paper: +[verity.pdf](https://lfglabs.dev/papers/verity.pdf) · MIT licensed. -

- A formally verified smart contract compiler for Ethereum, written in Lean 4.
- Documentation: veritylang.com  ·  Paper: verity.pdf  ·  Built by LFG Labs -

+## Guarantees -

- Verity documentation - MIT License - Built with Lean 4 - Verification status - Verify -

+- **Layer 1** (DSL → `CompilationModel`): per-contract bridge theorems. +- **Layer 2** (`CompilationModel` → IR): a generic whole-contract theorem covers the supported fragment with zero axioms. +- **Layer 3** (IR → Yul): every statement form is proved equivalent. The dispatch bridge is an explicit theorem hypothesis, not an axiom. ---- - -**Verity** is a formally verified smart contract compiler written in [Lean 4](https://lean-lang.org/). You write contracts in an embedded DSL, state what they should do, prove those properties hold, and compile to EVM bytecode. The compiler itself is proven to preserve semantics across three verified layers. Full documentation lives at [**veritylang.com**](https://veritylang.com). - -## Verification status - -All proofs are machine-checked by the Lean kernel. CI rebuilds the proof development on every commit, and repository checks enforce that no proof is left incomplete (no `sorry`) and that the compiler proof stack carries 0 axioms (see [AXIOMS.md](AXIOMS.md)). Verification is scoped rather than total: the generic compiler theorems cover an explicitly documented fragment of the language, and the precise boundary between what is proven and what is trusted is maintained in [TRUST_ASSUMPTIONS.md](TRUST_ASSUMPTIONS.md). - -A detailed proof inventory — theorem status, per-contract results, and test coverage — is regenerated from the codebase and tracked in [docs/VERIFICATION_STATUS.md](docs/VERIFICATION_STATUS.md). All checks are reproducible locally (see [Quick start](#quick-start)). - -## What is verified - -Verity proves that compilation preserves behavior at three stages. Each layer is a machine-checked Lean theorem. - -**Layer 1** (EDSL to CompilationModel): the `verity_contract` macro generates both an executable Lean program and a compiler-facing model from a single definition. Per-contract bridge theorems prove they agree. - -**Layer 2** (CompilationModel to IR): a generic whole-contract theorem covers the supported fragment with zero axioms. No per-contract proof effort needed. Internal helper calls now exist at the source level, and helper-summary proof reuse is available in source-semantics lemmas, but that reuse is not yet fully consumed through the generic body/IR theorem path. ECMs, typed interface calls, external calls, and low-level call/returndata mechanics are trust-reported or compiler-supported rather than fully proof-modeled. Constructors, fallback/receive, events/logs, typed errors, proxy/delegatecall, local obligations, and richer storage-layout features remain outside the generic proof fragment or partial. `forEach` support is deliberately partial: zero-bound loops with supported bodies and arbitrary literal-bound empty-body loops are proved, while positive non-empty loop bodies remain outside the current theorem. - -**Layer 3** (IR to Yul): all statement types are proven equivalent. The dispatch bridge is an explicit theorem hypothesis, not an axiom. - -The Yul-to-bytecode step is handled by `solc` (v0.8.33, pinned) and is not verified by Verity. See [TRUST_ASSUMPTIONS.md](TRUST_ASSUMPTIONS.md) for the complete trust boundary. - -## How it works - -``` -verity_contract Counter where - storage count : Uint256 := slot 0 - function increment () : Unit := do - let current <- getStorage count - setStorage count (add current 1) -``` - -```lean -theorem increment_correct (s : ContractState) : - let s' := ((increment).run s).snd - s'.storage 0 = add (s.storage 0) 1 := by rfl -``` - -The proof passes by `rfl` (reflexivity): Lean's kernel evaluates both sides and confirms they are definitionally equal. No external solver, no bounded model checker, no trust in anything beyond Lean's type theory. +The compiler proof stack carries 0 axioms and no `sorry`. Yul → bytecode is +done by pinned `solc` and is trusted. Verification is scoped: the proved +fragment and everything trusted are listed in the trust documents below. ## Quick start -```bash -curl https://raw.githubusercontent.com/leanprover/elan/master/elan-init.sh -sSf | sh -source ~/.elan/env -git clone https://github.com/lfglabs-dev/verity.git && cd verity -lake build # verify all proofs (~20 min first build) -make check # run full CI validation suite -FOUNDRY_PROFILE=difftest forge test # differential tests: EDSL vs EVM +```sh +lake build # check every proof +make check # repository checks run in CI (no Lean build) +make test-foundry ``` -## How Verity compares - -| | Certora | Halmos | Verity | -|---|---|---|---| -| **Approach** | Bounded model checking | Symbolic execution | Theorem proving in Lean 4 | -| **Proof scope** | Bounded (configurable depth) | Bounded (path explosion) | Unbounded (all inputs, all paths) | -| **Compiler trust** | Trusts solc entirely | Trusts solc entirely | Verifies 3 compilation layers | -| **Best for** | Production audits at scale | Bug-finding in Foundry | High-assurance contracts | - -Verity is complementary to these tools. It is for cases where you need mathematical certainty across all inputs and all execution paths. - -## Documentation - -| Resource | Description | -|----------|-------------| -| [veritylang.com](https://veritylang.com/) | Full documentation site | -| [Solidity to Verity](https://veritylang.com/guides/solidity-to-verity) | Practical syntax and semantic mappings for Solidity ports | -| [Production Solidity Patterns](https://veritylang.com/guides/production-solidity-patterns) | Agent guidance for production ports, reusable Verity features, and oracle/spec boundaries | -| [docs/ARCHITECTURE.md](docs/ARCHITECTURE.md) | Contributor map for source features, proof layers, trust surfaces, and regression checks | -| [docs/VERIFICATION_STATUS.md](docs/VERIFICATION_STATUS.md) | Theorem counts, proof status, test coverage | -| [docs/INTRINSICS.md](docs/INTRINSICS.md) | Consumer-owned opcode bindings and their trust model | -| [docs/LOW_LEVEL_YUL.md](docs/LOW_LEVEL_YUL.md) | Policy for typed low-level primitives vs. raw Yul escape hatches | -| [TRUST_ASSUMPTIONS.md](TRUST_ASSUMPTIONS.md) | What is verified vs. what is trusted | -| [AXIOMS.md](AXIOMS.md) | Documented axioms (currently 0) | -| [AUDIT.md](AUDIT.md) | Trust-boundary audit evidence and CI guards | -| [CONTRIBUTING.md](CONTRIBUTING.md) | Contribution guidelines | - -### Research +## Where things are -- [Verity: A Formally Verified Smart Contract Compiler](https://lfglabs.dev/papers/verity.pdf) -- [Verity Benchmark: AI-Driven Proof Generation](https://lfglabs.dev/research/verity-benchmark) -- [What is a formal proof?](https://lfglabs.dev/research/what-is-a-formal-proof) +| Path | Contents | +| --- | --- | +| `Verity/` | The DSL (`verity_contract`), its semantics and proof library | +| `Compiler/` | The compiler and its correctness proofs | +| `Compiler/SolidityImport/` | Solidity → `CompilationModel` importer ([docs](docs/SOLIDITY_IMPORT.md)) | +| `Contracts/` | Example and smoke contracts with their proofs | +| `docs/` | Trust boundary, axioms, verification status, design notes | +| `scripts/`, `test/`, `artifacts/` | CI checks, Foundry tests, generated reports | -## Support +## What to review -Verity is a public good. If you'd like to support the project, you can donate via Giveth: +1. [docs/TRUST_ASSUMPTIONS.md](docs/TRUST_ASSUMPTIONS.md): what is proved and what is trusted. +2. [docs/AXIOMS.md](docs/AXIOMS.md): every axiom, with its justification. +3. [docs/VERIFICATION_STATUS.md](docs/VERIFICATION_STATUS.md): theorem and test inventory, regenerated by CI. +4. [docs/SOLIDITY_IMPORT.md](docs/SOLIDITY_IMPORT.md): the Solidity importer and its differential validation. -[verity: Smart Contracts Security for the Age of AI](https://giveth.io/project/verity:-smart-contracts-security-for-the-age-of-ai) +Contributions: see [CONTRIBUTING.md](CONTRIBUTING.md). diff --git a/SUPPORTEDSPEC_CONVERSION.md b/SUPPORTEDSPEC_CONVERSION.md deleted file mode 100644 index ec22e544a9..0000000000 --- a/SUPPORTEDSPEC_CONVERSION.md +++ /dev/null @@ -1,193 +0,0 @@ -# `SupportedSpec.lean` → compiler-call conversion - -`Compiler/Proofs/IRGeneration/SupportedSpec.lean` historically hand-restates the -set of programs the compiler accepts as standalone `Prop`s or pattern-matching -`Bool`s. That style duplicates compiler logic: every time the compiler relaxes -or tightens a gate, the proof layer's enumeration silently drifts and the -"supported" envelope quietly diverges from what the compiler actually accepts. - -The repo already established a better pattern in the event-emission predicates, -where the proof-side support predicate is **defined by calling the actual -compiler gating function**, so the two cannot disagree by construction: - -```lean --- Compiler side (Compiler/CompilationModel/EventEmission.lean) -def eventParamScalarCompileSupported (ty : ParamType) : Bool := ... - --- Proof side (this file) -def eventParamScalarProofSupported (ty : ParamType) : Bool := - eventParamScalarCompileSupported ty -``` - -The proof-side name signals **intent** ("what proofs depend on for soundness"), -while the body delegates to the compile-side Bool that drives the compiler's -real branch. Downstream lemmas like -`eventParamScalarProofSupported_eventIsDynamicType_eq_false` then `simp` through -both names interchangeably. - -This PR converts two scalar/leaf predicates in `SupportedSpec.lean` to the same -shape, as an exemplar that the remaining ~70 hand-restated predicates in the -file can follow. - -## Predicates converted - -### 1. `SupportedExternalScalarParamType` → `externalParamScalarProofSupported` - -The hand-restated `Prop`: - -```lean -def SupportedExternalScalarParamType : ParamType → Prop - | .uint256 | .int256 | .uint8 | .uint16 | .address | .bytes32 | .bool => True - | _ => False -``` - -is the exact mirror of the compiler-side gating function in -`Compiler/CompilationModel/ParamLoading.lean`: - -```lean -def isScalarParamType : ParamType → Bool - | ParamType.uint256 | ParamType.int256 | ParamType.uint8 | ParamType.uint16 - | ParamType.address | ParamType.bool | ParamType.bytes32 => true - | _ => false -``` - -The new proof-side alias is one line: - -```lean -def externalParamScalarProofSupported (ty : ParamType) : Bool := - isScalarParamType ty -``` - -### 2. `SupportedExternalReturnProfile` → `externalReturnProfileProofSupported` - -The hand-restated `Prop`: - -```lean -def SupportedExternalReturnProfile : List ParamType → Prop - | [] => True - | [ty] => SupportedExternalScalarParamType ty - | _ => False -``` - -becomes a Bool that pushes the per-element decision down to the compiler: - -```lean -def externalReturnProfileProofSupported (returns : List ParamType) : Bool := - decide (returns.length ≤ 1) && returns.all isScalarParamType -``` - -## Agreement theorems - -Because the heavily-used hand-restated predicates currently sit in proof -preconditions (`∀ param ∈ params, SupportedExternalScalarParamType param.ty`) across -many sibling modules, we **keep** the old defs and prove -`old ↔ new` instead of rewriting every caller in one shot. The biconditionals -are the meaning-preservation oracle: - -```lean -theorem SupportedExternalParamType_iff_externalParamScalarProofSupported - (ty : ParamType) : - SupportedExternalScalarParamType ty ↔ externalParamScalarProofSupported ty = true - -theorem SupportedExternalReturnProfile_iff_externalReturnProfileProofSupported - (returns : List ParamType) : - SupportedExternalReturnProfile returns ↔ - externalReturnProfileProofSupported returns = true -``` - -Both proofs are a single `cases ... <;> simp [...]` / `match ... with => simp` -because the cases line up. No `sorry`, no new `axiom`, no `native_decide`. - -## Recipe to replicate across remaining predicates - -For each hand-restated proof-side predicate `P` in `SupportedSpec.lean` (or a -sibling proof module): - -1. **Find the compiler-side gating function.** Grep the `CompilationModel` - directory for a `Bool` (or `Except` / `Option`-returning) function whose - match arms classify the same syntactic cases as `P`. For ParamType-level - leaves the usual suspects are `isScalarParamType`, - `isSingleWordStaticParamType`, `isDynamicParamType`, `isWordArrayParam`, - `internalDynamicParamSupported`, `supportedCustomErrorParamType`, - `eventParamScalarCompileSupported`, and `indexedDynamicArrayElemSupported`. - For expression / statement predicates the gate is typically the `compile*` - function whose `Except String` failure path encodes the unsupported cases — - in which case the proof-side wrapper checks `.isOk` / `.isSome`. - -2. **Introduce a thin proof-side wrapper.** Add a single-line - `def fooProofSupported ... := compilerGate ...` whose body is just the - compiler call. The naming convention is `ProofSupported` - to match `eventParamScalarProofSupported`. Place it near the existing - predicates so the pattern is locally visible. - -3. **Prove agreement with the hand-restated form.** Add - `theorem P_iff_fooProofSupported : P x ↔ fooProofSupported x = true`. For - leaf scalar/leaf predicates this is a one-liner: - `cases x <;> simp [P, fooProofSupported, compilerGate]`. For predicates - over lists or composite IR, recurse with `match`/`induction` and reuse the - leaf agreement lemma. For `Except String`-shaped gates, the agreement - lemma is `P x ↔ (compilerGate x).isOk` and the proof works by case-splitting - on the `Except` result. - -4. **Decide on retention.** If `P` is referenced widely outside the file (use - `grep -rn P --include='*.lean'`), keep `P` and only add the agreement - theorem — the lemma is enough to feed both directions into existing proofs. - If `P` has few callers, inline-replace each caller with the new wrapper and - delete `P`; the agreement theorem then degrades into a self-test you can - keep or drop. - -5. **Build the affected module.** `lake build - Compiler.Proofs.IRGeneration.SupportedSpec` (or the sibling module). - Iterate until clean: no `sorry`, no new `axiom`, no `native_decide`. The - typical failure modes are: - - `simp` can't close a case because the compiler bool depends on a helper - that isn't unfolded yet. Add it to the `simp` set. - - The compiler bool is strictly broader/narrower than the hand version. - This is a **drift bug** — fix the spec to match the compiler, or - restrict the new wrapper with `&& extraCondition`. - - The compiler gate is `Except`/`Option`-shaped. Don't `simp` the failure - branch away; pattern-match it and prove the failure case maps to `P x = False`. - -6. **Once a critical mass of predicates is converted**, audit the resulting - `SupportedSpec` and consider deleting the original hand-restated Props - entirely, since every call site that used `P x` now factors through - `P_iff_fooProofSupported` to talk about `fooProofSupported x = true`. - -## Module build status — BLOCKED on pre-existing breakage - -`lake build Compiler.Proofs.IRGeneration.SupportedSpec` was run after the -edits and **failed**, but not because of this conversion. The branch this work -sits on top of has a pre-existing missing-case error: a recent commit added a -new `Expr.txOrigin` variant (see commits introducing `txOrigin` in -`Compiler/CompilationModel/UsageAnalysis.lean`, `LogicalPurity.lean`, -`ValidationInterop.lean`, etc.) but the proof-side files that pattern-match -exhaustively on `Expr` were never updated. The first build failure surfaces -in `Compiler/Proofs/IRGeneration/ExprCore.lean:18` (the `exprBoundNames` -mutual block): - -``` -error: Compiler/Proofs/IRGeneration/ExprCore.lean:18:2: Missing cases: -Expr.txOrigin -error: Lean exited with code 1 -``` - -`SupportedSpec.lean` itself has approximately ten further exhaustive `Expr` -matches that would also need `.txOrigin` added (every `exprTouchesUnsupported*Surface` -function around lines 604, 754, 810, 863, 922, 990, 1053, 1114, 1176, 1896). - -Fixing all of these is outside the conversion scope this task was chartered -for (replicating the `eventParamScalarCompileSupported` pattern). The two -new defs (`externalParamScalarProofSupported`, -`externalReturnProfileProofSupported`) and the two agreement theorems were -verified by inspection — they each rely only on `simp` with the relevant -definitions in scope and follow the same recipe that's already known to -compile for `eventParamScalarProofSupported`. They contain no `sorry`, no -new `axiom`, and no `native_decide`. - -**Recommended unblock path (separate, focused PR):** sweep -`Compiler/Proofs/IRGeneration/{ExprCore.lean, SupportedSpec.lean, FunctionBody.lean, SourceSemantics.lean, ...}` -and add `.txOrigin` to every exhaustive `Expr` enumeration, mirroring the -existing `.caller`/`.contractAddress`/`.chainid` handling (zero bound names, -no helper calls, no unsupported surface). Once that lands, -`lake build Compiler.Proofs.IRGeneration.SupportedSpec` should pick up the -work here and the two agreement theorems should close as written. diff --git a/StorageLayoutReport.lean b/StorageLayoutReport.lean index b8444b2f21..9c96032ff4 100644 --- a/StorageLayoutReport.lean +++ b/StorageLayoutReport.lean @@ -14,7 +14,7 @@ inside `Compiler/`. `PrintAxioms.lean` uses the same root-level placement for the same reason. - See AUDIT.md for the audit-artifact registry. + See docs/TRUST_ASSUMPTIONS.md for the trust boundary. -/ import Compiler.CompilationModel import Compiler.CompilationModel.LayoutReport diff --git a/Verity/EVM/Frame.lean b/Verity/EVM/Frame.lean index 84bc78db49..6890147c9a 100644 --- a/Verity/EVM/Frame.lean +++ b/Verity/EVM/Frame.lean @@ -28,7 +28,7 @@ Before this module, every benchmark that touched external calls had to assume (or reproduce in their own file) the EVM-level frame condition that external `CALL` cannot SSTORE the caller's slots and cannot write to caller memory outside the declared output buffer. That assumption is -documented in `AXIOMS.md` under "External Call Module". This module +documented in `docs/AXIOMS.md` under "External Call Module". This module makes the frame condition a theorem of `Verity.EVM`, parameterised over an abstract finite `CalleeResult` observable. diff --git a/artifacts/COMPILER_ARTIFACTS.md b/artifacts/COMPILER_ARTIFACTS.md deleted file mode 100644 index 774e5d4d3d..0000000000 --- a/artifacts/COMPILER_ARTIFACTS.md +++ /dev/null @@ -1,30 +0,0 @@ -# Compiler Artifacts - -This folder collects compiler artifacts and fixtures used by tests, CI, and documentation. - -## Contents - -- `yul/`: Generated Yul output used by Foundry tests and differential testing. Generated by `lake exe verity-compiler`. -- `macro_property_tests/`: Auto-generated Foundry property test stubs for all `verity_contract` contracts. Generated by `scripts/generate_macro_property_tests.py`. -- `verification_status.json`: Machine-readable verification metrics (theorem counts, axiom counts). Generated by `scripts/generate_verification_status.py`. -- `layer2_boundary_catalog.json`: Machine-readable Layer 2 theorem target and proof-boundary catalog. Generated by `scripts/generate_layer2_boundary_catalog.py`. -- `lean_warning_baseline.json`: Lean warning regression baseline. Consumed by `scripts/check_lean_warning_regression.py`. -- `interpreter_feature_matrix.json`: Machine-readable interpreter feature support matrix. -- `evmyullean_capability_report.json`: EVMYulLean builtin coverage report. Generated by `scripts/generate_evmyullean_capability_report.py`. -- `trust_surface_report.json`: Machine-readable trust-surface inventory (non-axiom trusted mechanisms, ECM assumption strings with source locations, Lean 4.31 kernel string-fact boundary). Generated by `scripts/generate_trust_surface_report.py`. -- `evmyullean_native_lowering_report.json`: EVMYulLean native lowering and bridge report. -- `evmyullean_unsupported_nodes.json`: EVMYulLean unsupported AST node report. -- `issue_1060_progress.json`: Issue #1060 migration tracking ledger (complete). - -## Related Source - -- Compiler implementation: `Compiler/` -- Proofs: `Compiler/Proofs/` -- Tests: `test/` (Foundry unit, property, and differential tests) - -## Assumptions - -Yul is compiled to bytecode using `solc` (trusted), and the runtime semantics are modeled in Lean. See: - -- Native EVMYulLean runtime proof boundary: `Compiler/Proofs/YulGeneration/Backends/EvmYulLeanNativeHarness.lean` -- Native lowering coverage: `Compiler/Proofs/YulGeneration/Backends/EvmYulLeanNativeLowering.lean` diff --git a/artifacts/STORAGE_LAYOUT_SUMMARY.md b/artifacts/STORAGE_LAYOUT_SUMMARY.md index 9d8e76bc3d..4386ebc971 100644 --- a/artifacts/STORAGE_LAYOUT_SUMMARY.md +++ b/artifacts/STORAGE_LAYOUT_SUMMARY.md @@ -5,7 +5,7 @@ _Auto-generated by `scripts/generate_storage_layout_report.py` from the generator after any change to a production storage layout._ Source of truth: `Compiler.Specs.allSpecs` (canonical production -contract surface). See `AUDIT.md` for the audit-artifact registry +contract surface). See `docs/TRUST_ASSUMPTIONS.md` for the audit-artifact registry and issue #1897 for the migration-review motivation. ## Non-alias certificate justifications (#1966) diff --git a/artifacts/issue_1060_progress.json b/artifacts/issue_1060_progress.json index 5487dc18da..302b5364d4 100644 --- a/artifacts/issue_1060_progress.json +++ b/artifacts/issue_1060_progress.json @@ -1924,8 +1924,8 @@ "AXIOMS.md and TRUST_ASSUMPTIONS.md accurately reflect zero-sorry state" ], "files_changed": [ - "AXIOMS.md", - "TRUST_ASSUMPTIONS.md" + "docs/AXIOMS.md", + "docs/TRUST_ASSUMPTIONS.md" ], "verification_commands": [ "lake build", diff --git a/artifacts/trust_surface_report.json b/artifacts/trust_surface_report.json index 9489b0b058..9669ccef7e 100644 --- a/artifacts/trust_surface_report.json +++ b/artifacts/trust_surface_report.json @@ -171,6 +171,6 @@ "native_decide": 613, "partial def": 195 }, - "notes": "native_decide trusts Lean.ofReduceBool or Lean 4.31 generated per-proof native_decide axioms + Lean.trustCompiler. Prose registry: AXIOMS.md, TRUST_ASSUMPTIONS.md (enforced by scripts/check_trust_surface_registry.py).", + "notes": "native_decide trusts Lean.ofReduceBool or Lean 4.31 generated per-proof native_decide axioms + Lean.trustCompiler. Prose registry: docs/AXIOMS.md, docs/TRUST_ASSUMPTIONS.md (enforced by scripts/check_trust_surface_registry.py).", "schema_version": 1 } diff --git a/docs-site/artifacts/screenshots/dark-code-page.png b/docs-site/artifacts/screenshots/dark-code-page.png deleted file mode 100644 index 21263ceaf8e02f10ac81c7e1792387f73da0e195..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 3128481 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 uF8qD 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