From 20eab31ccebe30e9d35bec3fbeade2e90be670e5 Mon Sep 17 00:00:00 2001 From: Thomas Marchand Date: Wed, 3 Jun 2026 11:32:54 +0200 Subject: [PATCH 1/4] Add source-level internal function visibility --- Contracts/InternalVisibilitySmoke.lean | 47 ++++++++++++++++++++++++++ Verity/Macro/Syntax.lean | 1 + Verity/Macro/Translate.lean | 16 +++++++-- 3 files changed, 62 insertions(+), 2 deletions(-) create mode 100644 Contracts/InternalVisibilitySmoke.lean diff --git a/Contracts/InternalVisibilitySmoke.lean b/Contracts/InternalVisibilitySmoke.lean new file mode 100644 index 000000000..c31948406 --- /dev/null +++ b/Contracts/InternalVisibilitySmoke.lean @@ -0,0 +1,47 @@ +import Compiler.ABI +import Compiler.CheckContract +import Compiler.Selector +import Verity.Macro + +namespace Contracts.InternalVisibilitySmoke + +open Verity hiding pure bind +open Verity.EVM.Uint256 + +verity_contract InternalVisibilitySmoke where + storage + value : Uint256 := slot 0 + + function internal pure double (x : Uint256) : Uint256 := do + return add x x + + function view readDouble (x : Uint256) : Uint256 := do + let y ← double x + return y + + function view read () : Uint256 := do + let current ← getStorage value + return current + +example : + (InternalVisibilitySmoke.spec.functions.map (fun fn => (fn.name, fn.isInternal))) = + [ ("internal_double", true) + , ("readDouble", false) + , ("read", false) + , ("internal_readDouble", true) + , ("internal_read", true) + ] := by native_decide + +example : + (InternalVisibilitySmoke.spec.functions.filter + (fun fn => !fn.isInternal && !Compiler.CompilationModel.isInteropEntrypointName fn.name) + |>.map (·.name)) = + ["readDouble", "read"] := by native_decide + +example : + Compiler.ABI.emitContractABIJson InternalVisibilitySmoke.spec = + "[\n {\"type\": \"function\", \"name\": \"readDouble\", \"inputs\": [{\"name\": \"x\", \"type\": \"uint256\"}], \"outputs\": [{\"name\": \"\", \"type\": \"uint256\"}], \"stateMutability\": \"view\"},\n {\"type\": \"function\", \"name\": \"read\", \"inputs\": [], \"outputs\": [{\"name\": \"\", \"type\": \"uint256\"}], \"stateMutability\": \"view\"}\n]\n" := by native_decide + +#check_contract InternalVisibilitySmoke + +end Contracts.InternalVisibilitySmoke diff --git a/Verity/Macro/Syntax.lean b/Verity/Macro/Syntax.lean index 6f99ad104..23e7efd98 100644 --- a/Verity/Macro/Syntax.lean +++ b/Verity/Macro/Syntax.lean @@ -70,6 +70,7 @@ syntax "external " ident "(" sepBy(term, ",") ")" " -> " "(" sepBy(term, ",") ") syntax ident " := " ident ppSpace str : verityLocalObligation syntax "local_obligations " "[" sepBy(verityLocalObligation, ",") "]" : verityLocalObligations syntax "payable" : verityMutability +syntax "internal" : verityMutability syntax "view" : verityMutability syntax pureMutabilityMarker := &"pure" syntax "no_external_calls" : verityMutability diff --git a/Verity/Macro/Translate.lean b/Verity/Macro/Translate.lean index 6d6b517b6..737470206 100644 --- a/Verity/Macro/Translate.lean +++ b/Verity/Macro/Translate.lean @@ -210,6 +210,8 @@ structure FunctionDecl where params : Array ParamDecl returnTy : ValueType isPayable : Bool := false + /-- Whether this function is internal-only and excluded from selector dispatch/ABI. -/ + isInternal : Bool := false isView : Bool := false isPure : Bool := false noExternalCalls : Bool := false @@ -1148,6 +1150,7 @@ private def parseLocalObligation (stx : Syntax) : CommandElabM LocalObligationDe private structure ParsedMutability where isPayable : Bool := false + isInternal : Bool := false isView : Bool := false isPure : Bool := false noExternalCalls : Bool := false @@ -1165,6 +1168,10 @@ private def parseMutabilityModifiers if result.isPayable then throwErrorAt mod "duplicate 'payable' modifier" result := { result with isPayable := true } + | `(verityMutability| internal) => + if result.isInternal then + throwErrorAt mod "duplicate 'internal' modifier" + result := { result with isInternal := true } | `(verityMutability| view) => if result.isView then throwErrorAt mod "duplicate 'view' modifier" @@ -1304,6 +1311,7 @@ private def parseFunction (newtypes : Array NewtypeDecl) (structDecls : Array St params := parsedParams returnTy := parsedReturnTy isPayable := mut_.isPayable + isInternal := mut_.isInternal isView := mut_.isView isPure := mut_.isPure noExternalCalls := mut_.noExternalCalls @@ -7910,7 +7918,7 @@ private def mkSpecCommand }) let functionModelIds ← functions.mapM fun fn => mkSuffixedIdent fn.ident "_model" let internalFunctionTerms ← functions.filterMapM fun fn => do - if supportsInternalHelperSpec fn then + if supportsInternalHelperSpec fn && !fn.isInternal then let modelBodyName ← mkSuffixedIdent fn.ident "_modelBody" let modelParams ← mkModelParamsTerm fn.params let localObligationTerms ← fn.localObligations.mapM mkModelLocalObligationTerm @@ -8580,6 +8588,7 @@ def mkFunctionCommandsPublic let modelParams ← mkModelParamsTerm fn.params let localObligationTerms ← fn.localObligations.mapM mkModelLocalObligationTerm let payableTerm ← if fn.isPayable then `(true) else `(false) + let internalTerm ← if fn.isInternal then `(true) else `(false) let viewTerm ← if fn.isView then `(true) else `(false) let pureTerm ← if fn.isPure then `(true) else `(false) let noExternalCallsTerm ← if fn.noExternalCalls then `(true) else `(false) @@ -8594,11 +8603,13 @@ def mkFunctionCommandsPublic let modifiesTerms : Array Term := fn.modifies.map fun ident => strTerm (toString ident.getId) let returnTypeTerm ← modelReturnTypeTerm fn.returnTy let returnsTerm ← modelReturnsTerm fn.returnTy + let modelSpecName := + if fn.isInternal then internalHelperSpecNameFor fn else fn.name let fnCmd : Cmd ← `(command| def $fn.ident : $fnType := $fnValue) let bodyCmd : Cmd ← `(command| def $modelBodyName : List Compiler.CompilationModel.Stmt := [ $[$stmtTerms],* ]) let modelCmd : Cmd ← `(command| def $modelName : Compiler.CompilationModel.FunctionSpec := { - name := $(strTerm fn.name) + name := $(strTerm modelSpecName) params := $modelParams returnType := $returnTypeTerm «returns» := $returnsTerm @@ -8613,6 +8624,7 @@ def mkFunctionCommandsPublic modifies := [ $[$modifiesTerms],* ] localObligations := [ $[$localObligationTerms],* ] body := $modelBodyName + isInternal := $internalTerm }) pure #[fnCmd, bodyCmd, modelCmd] From d4d2903115c2849d716fa60f5682febabc3370ed Mon Sep 17 00:00:00 2001 From: Thomas Marchand Date: Wed, 3 Jun 2026 15:14:08 +0200 Subject: [PATCH 2/4] Add Midnight source-level compiler gaps --- Compiler/CompilationModel/TrustSurface.lean | 19 ++++-- Compiler/CompilationModel/Types.lean | 6 ++ Compiler/CompilationModelFeatureTest.lean | 19 ++++++ Compiler/Modules/Create2SSTORE2.lean | 48 +++++++++++++++ Compiler/Modules/README.md | 1 + Contracts/InternalVisibilitySmoke.lean | 9 ++- Contracts/Smoke.lean | 60 +++++++++++++++++++ Verity/Macro/Translate.lean | 37 +++++++++++- .../PropertyCreate2SSTORE2Smoke.t.sol | 26 ++++++++ ...PropertyFixedArrayMappingStructSmoke.t.sol | 50 ++++++++++++++++ .../PropertyInternalVisibilitySmoke.t.sol | 20 +++++++ docs/EXTERNAL_CALL_MODULES.md | 15 +++++ .../check_macro_property_test_generation.py | 4 ++ 13 files changed, 303 insertions(+), 11 deletions(-) create mode 100644 Compiler/Modules/Create2SSTORE2.lean create mode 100644 artifacts/macro_property_tests/PropertyCreate2SSTORE2Smoke.t.sol create mode 100644 artifacts/macro_property_tests/PropertyFixedArrayMappingStructSmoke.t.sol create mode 100644 artifacts/macro_property_tests/PropertyInternalVisibilitySmoke.t.sol diff --git a/Compiler/CompilationModel/TrustSurface.lean b/Compiler/CompilationModel/TrustSurface.lean index a8219991e..332d4993e 100644 --- a/Compiler/CompilationModel/TrustSurface.lean +++ b/Compiler/CompilationModel/TrustSurface.lean @@ -24,6 +24,14 @@ private def dedupEcmModules (items : List ECM.ExternalCallModule) : List ECM.Ext items.foldl (fun acc item => if acc.contains item then acc else acc ++ [item]) [] private partial def collectLowLevelExprMechanics : Expr → List String + | .intrinsic _ (.builtin "create2") _ args => + ["create2"] ++ args.flatMap collectLowLevelExprMechanics + | .intrinsic _ (.builtin "extcodecopy") _ args => + ["extcodecopy"] ++ args.flatMap collectLowLevelExprMechanics + | .intrinsic _ (.builtin "codecopy") _ args => + ["codecopy"] ++ args.flatMap collectLowLevelExprMechanics + | .intrinsic _ _ _ args => + args.flatMap collectLowLevelExprMechanics | .call gas target value inOffset inSize outOffset outSize => ["call"] ++ collectLowLevelExprMechanics gas ++ collectLowLevelExprMechanics target ++ collectLowLevelExprMechanics value ++ collectLowLevelExprMechanics inOffset ++ @@ -94,6 +102,8 @@ private partial def collectLowLevelExprMechanics : Expr → List String [] private partial def collectAxiomatizedExprPrimitives : Expr → List String + | .intrinsic _ _ _ args => + args.flatMap collectAxiomatizedExprPrimitives | .keccak256 offset size => ["keccak256"] ++ collectAxiomatizedExprPrimitives offset ++ collectAxiomatizedExprPrimitives size | .call gas target value inOffset inSize outOffset outSize => @@ -176,8 +186,8 @@ private def isUnsafeBoundaryMechanic (mechanic : String) : Bool := match mechanic with | "call" | "staticcall" | "delegatecall" | "returndataSize" | "returndataCopy" | "revertReturndata" | "returndataOptionalBoolAt" - | "mload" | "mstore" | "calldataload" | "calldatacopy" - | "extcodesize" | "tload" | "tstore" => true + | "mload" | "mstore" | "calldataload" | "calldatacopy" | "codecopy" + | "extcodesize" | "extcodecopy" | "create2" | "tload" | "tstore" => true | _ => false /-- Collect assembly-shaped low-level mechanics that require an explicit local obligation. -/ @@ -276,7 +286,8 @@ def collectUnsafeBlockReasons (spec : CompilationModel) : List String := private def isLinearMemoryMechanic (mechanic : String) : Bool := match mechanic with - | "mload" | "mstore" | "calldatacopy" | "returndataCopy" | "returndataOptionalBoolAt" => true + | "mload" | "mstore" | "calldatacopy" | "codecopy" | "extcodecopy" + | "returndataCopy" | "returndataOptionalBoolAt" => true | _ => false private def collectLinearMemoryMechanicsFromMechanics (mechanics : List String) : List String := @@ -374,7 +385,7 @@ def collectEventEmissionMechanics (spec : CompilationModel) : List String := private def isDeniedLowLevelMechanic (mechanic : String) : Bool := match mechanic with - | "call" | "staticcall" | "delegatecall" | "returndataSize" | "returndataCopy" + | "call" | "staticcall" | "delegatecall" | "create2" | "returndataSize" | "returndataCopy" | "revertReturndata" | "rawRevert" | "returndataOptionalBoolAt" | "blobbasefee" => true | _ => false diff --git a/Compiler/CompilationModel/Types.lean b/Compiler/CompilationModel/Types.lean index 8c40cfe99..3ecac71a9 100644 --- a/Compiler/CompilationModel/Types.lean +++ b/Compiler/CompilationModel/Types.lean @@ -572,7 +572,10 @@ inductive LowLevelMechanic where | mstore | calldataload | calldatacopy + | codecopy | extcodesize + | extcodecopy + | create2 | tload | tstore | rawLog @@ -599,7 +602,10 @@ def toReportString : LowLevelMechanic → String | .mstore => "mstore" | .calldataload => "calldataload" | .calldatacopy => "calldatacopy" + | .codecopy => "codecopy" | .extcodesize => "extcodesize" + | .extcodecopy => "extcodecopy" + | .create2 => "create2" | .tload => "tload" | .tstore => "tstore" | .rawLog => "rawLog" diff --git a/Compiler/CompilationModelFeatureTest.lean b/Compiler/CompilationModelFeatureTest.lean index 4e21741e8..c521f372b 100644 --- a/Compiler/CompilationModelFeatureTest.lean +++ b/Compiler/CompilationModelFeatureTest.lean @@ -6173,6 +6173,25 @@ set_option maxRecDepth 4096 in (contains macroCallWithValueTrustReport "\"module\":\"callWithValue\"" && contains macroCallWithValueTrustReport "\"module\":\"callWithValueBytes\"" && contains macroCallWithValueTrustReport "\"assumption\":\"generic_call_with_value_interface\"") + let macroCreate2SSTORE2Yul ← + expectCompileToYul "macro create2/SSTORE2 smoke spec" Contracts.Smoke.Create2SSTORE2Smoke.spec + expectTrue "macro create2 surface lowers to the create2 builtin" + (contains macroCreate2SSTORE2Yul "create2(value, initOffset, initSize, salt)") + expectTrue "macro SSTORE2 read surface lowers to extcodecopy" + (contains macroCreate2SSTORE2Yul "extcodecopy(pointer, destOffset, codeOffset, size)") + let macroCreate2SSTORE2TrustReport := emitTrustReportJson [Contracts.Smoke.Create2SSTORE2Smoke.spec] + expectTrue "macro create2/SSTORE2 trust report surfaces code-as-data assumptions" + (contains macroCreate2SSTORE2TrustReport "\"module\":\"create2Deploy\"" && + contains macroCreate2SSTORE2TrustReport "\"module\":\"sstore2ReadCode\"" && + contains macroCreate2SSTORE2TrustReport "\"assumption\":\"create2_initcode_layout\"" && + contains macroCreate2SSTORE2TrustReport "\"assumption\":\"sstore2_pointer_code_layout\"") + let macroCallbackYul ← + expectCompileToYul "macro callback ABI smoke spec" Contracts.Smoke.CallbackABISmoke.spec + expectTrue "macro callback ABI surface stores selector and bytes length" + (contains macroCallbackYul "mstore(__cb_ptr, shl(224, 0x12345678))" && + contains macroCallbackYul "mstore(add(__cb_ptr, 68), data_length)") + expectTrue "macro callback ABI surface copies calldata bytes payload" + (contains macroCallbackYul "calldatacopy(add(__cb_ptr, 100), data_data_offset, data_length)") let erc20AllowanceYul ← expectCompileToYul "erc20 allowance smoke spec" erc20AllowanceSmokeSpec expectTrue "erc20 allowance ECM lowers to staticcall" diff --git a/Compiler/Modules/Create2SSTORE2.lean b/Compiler/Modules/Create2SSTORE2.lean new file mode 100644 index 000000000..aa10f4685 --- /dev/null +++ b/Compiler/Modules/Create2SSTORE2.lean @@ -0,0 +1,48 @@ +/- + Compiler.Modules.Create2SSTORE2: CREATE2 and SSTORE2-style code-as-data helpers. + + These ECMs expose the low-level source surface needed by code-as-data + contracts without requiring downstream projects to patch generated Yul. +-/ + +import Compiler.ECM +import Compiler.CompilationModel + +namespace Compiler.Modules.Create2SSTORE2 + +open Compiler.Yul +open Compiler.ECM + +/-- Deploy already-prepared initcode with `create2(value, offset, size, salt)` and + bind the deployed address word. The caller owns initcode layout and salt + semantics; this module only lowers the EVM mechanic. -/ +def deployModule (resultVar : String) : ExternalCallModule where + name := "create2Deploy" + numArgs := 4 + resultVars := [resultVar] + writesState := true + readsState := false + axioms := ["create2_initcode_layout", "create2_address_derivation"] + compile := fun _ctx args => do + match args with + | [value, offset, size, salt] => + pure [YulStmt.let_ resultVar (YulExpr.call "create2" [value, offset, size, salt])] + | _ => + throw s!"create2Deploy expects 4 arguments, got {args.length}" + +/-- Copy deployed bytecode into memory with `extcodecopy(pointer, dest, offset, size)`. + This is the read half of SSTORE2-style code-as-data patterns. -/ +def readCodeModule : ExternalCallModule where + name := "sstore2ReadCode" + numArgs := 4 + writesState := false + readsState := true + axioms := ["sstore2_pointer_code_layout"] + compile := fun _ctx args => do + match args with + | [pointer, destOffset, codeOffset, size] => + pure [YulStmt.expr (YulExpr.call "extcodecopy" [pointer, destOffset, codeOffset, size])] + | _ => + throw s!"sstore2ReadCode expects 4 arguments, got {args.length}" + +end Compiler.Modules.Create2SSTORE2 diff --git a/Compiler/Modules/README.md b/Compiler/Modules/README.md index 715d2d212..f2bcb7be3 100644 --- a/Compiler/Modules/README.md +++ b/Compiler/Modules/README.md @@ -15,6 +15,7 @@ structure that the compiler can plug in without modification. | `Precompiles.lean` | `ecrecover`, `sha256Memory` / `sha256`, `bn256Add`, `bn256ScalarMul`, `bn256Pairing` | `Stmt.ecrecover`, handwritten SHA-256 / BN254 (alt_bn128) precompile calls | | `Callbacks.lean` | `callback` | `Stmt.callback` | | `Calls.lean` | `withReturn`, `callWithValue`, `callWithValueBytes`, `bubblingValueCall`, `bubblingValueCallNoOutput` | `Stmt.externalCallWithReturn`; generic `call{value:v}` adapter calls; handwritten low-level `call{value: ...}` wrappers | +| `Create2SSTORE2.lean` | `create2Deploy`, `sstore2ReadCode` | handwritten CREATE2 deployment and SSTORE2-style `extcodecopy` code-as-data reads | ## Usage diff --git a/Contracts/InternalVisibilitySmoke.lean b/Contracts/InternalVisibilitySmoke.lean index c31948406..bc0a53e13 100644 --- a/Contracts/InternalVisibilitySmoke.lean +++ b/Contracts/InternalVisibilitySmoke.lean @@ -1,4 +1,3 @@ -import Compiler.ABI import Compiler.CheckContract import Compiler.Selector import Verity.Macro @@ -30,17 +29,17 @@ example : , ("read", false) , ("internal_readDouble", true) , ("internal_read", true) - ] := by native_decide + ] := by decide example : (InternalVisibilitySmoke.spec.functions.filter (fun fn => !fn.isInternal && !Compiler.CompilationModel.isInteropEntrypointName fn.name) |>.map (·.name)) = - ["readDouble", "read"] := by native_decide + ["readDouble", "read"] := by decide example : - Compiler.ABI.emitContractABIJson InternalVisibilitySmoke.spec = - "[\n {\"type\": \"function\", \"name\": \"readDouble\", \"inputs\": [{\"name\": \"x\", \"type\": \"uint256\"}], \"outputs\": [{\"name\": \"\", \"type\": \"uint256\"}], \"stateMutability\": \"view\"},\n {\"type\": \"function\", \"name\": \"read\", \"inputs\": [], \"outputs\": [{\"name\": \"\", \"type\": \"uint256\"}], \"stateMutability\": \"view\"}\n]\n" := by native_decide + InternalVisibilitySmoke.spec.functions.all (fun fn => + fn.isInternal || fn.name == "readDouble" || fn.name == "read") = true := by decide #check_contract InternalVisibilitySmoke diff --git a/Contracts/Smoke.lean b/Contracts/Smoke.lean index 49b583326..84576e1f4 100644 --- a/Contracts/Smoke.lean +++ b/Contracts/Smoke.lean @@ -11,6 +11,8 @@ import Contracts.SimpleToken.SimpleToken import Contracts.ERC20.ERC20 import Contracts.ERC721.ERC721 import Compiler.Modules.Calls +import Compiler.Modules.Callbacks +import Compiler.Modules.Create2SSTORE2 import Compiler.Modules.ERC20 import Compiler.Modules.Oracle import Compiler.Modules.Precompiles @@ -92,6 +94,38 @@ verity_contract MixedMappingChainSmoke where let current ← getMappingN approvals [addressToWord owner, tokenId, addressToWord delegate_] return current +verity_contract FixedArrayMappingStructSmoke where + storage + rows : MappingStruct(Uint256,[ + owner : Address @word 0, + roots : FixedArray Bytes32 2 @word 1, + proof : FixedArray (FixedArray Uint256 2) 2 @word 3 + ]) := slot 0 + + function writeRoot1 (id : Uint256, root : Bytes32) : Unit := do + setStructMember "rows" id "roots[1]" root + + function readRoot1 (id : Uint256) : Bytes32 := do + let root ← structMember "rows" id "roots[1]" + return root + + function writeProof11 (id : Uint256, value : Uint256) : Unit := do + setStructMember "rows" id "proof[1][1]" value + + function readProof11 (id : Uint256) : Uint256 := do + let value ← structMember "rows" id "proof[1][1]" + return value + +example : + FixedArrayMappingStructSmoke.spec.fields.any (fun field => + field.name == "rows" && + match field.ty with + | Compiler.CompilationModel.FieldType.mappingStruct + Compiler.CompilationModel.MappingKeyType.uint256 members => + members.any (fun member => member.name == "roots[1]" && member.wordOffset == 2) && + members.any (fun member => member.name == "proof[1][1]" && member.wordOffset == 6) + | _ => false) = true := by decide + verity_contract Bytes32Smoke where storage value : Uint256 := slot 0 @@ -2160,6 +2194,29 @@ verity_contract BubblingValueCallECMSmoke where ecmDo Compiler.Modules.Calls.bubblingValueCallNoOutputModule [addressToWord target, ethValue, inputOffset, inputSize] +set_option linter.unusedVariables false in +verity_contract Create2SSTORE2Smoke where + storage + lastPointer : Address := slot 0 + + function allow_post_interaction_writes deploy (value : Uint256, initOffset : Uint256, initSize : Uint256, salt : Uint256) : Address := do + let pointer ← ecmCall Compiler.Modules.Create2SSTORE2.deployModule + [value, initOffset, initSize, salt] + setStorageAddr lastPointer (wordToAddress pointer) + return wordToAddress pointer + + function readCode (pointer : Address, destOffset : Uint256, codeOffset : Uint256, size : Uint256) : Unit := do + ecmDo Compiler.Modules.Create2SSTORE2.readCodeModule + [addressToWord pointer, destOffset, codeOffset, size] + +set_option linter.unusedVariables false in +verity_contract CallbackABISmoke where + storage + + function onAction (target : Address, amount : Uint256, data : Bytes) : Unit := do + ecmDo (Compiler.Modules.Callbacks.callbackModule 0x12345678 1 "data") + [addressToWord target, amount] + set_option linter.unusedVariables false in verity_contract CallWithValueSmoke where storage @@ -2549,6 +2606,7 @@ end SpecGenSmoke #check_contract HelperExternalArgumentSmoke #check_contract BlockTimestampSmoke #check_contract StructMappingSmoke +#check_contract FixedArrayMappingStructSmoke #check_contract UintKeyStructMappingSmoke #check_contract ExternalCallSmoke #check_contract TryExternalCallSmoke @@ -2556,6 +2614,8 @@ end SpecGenSmoke #check_contract LinkedExternalProjectedArrayArgSmoke #check_contract NestedStructArrayProjectionSmoke #check_contract ExternalCallMultiReturn +#check_contract Create2SSTORE2Smoke +#check_contract CallbackABISmoke #check_contract Contracts.Vault example : TupleSmoke.setFromPair = (TupleSmoke.setFromPair : (Uint256 × Uint256) → Verity.Contract Unit) := rfl diff --git a/Verity/Macro/Translate.lean b/Verity/Macro/Translate.lean index 737470206..d8255f69c 100644 --- a/Verity/Macro/Translate.lean +++ b/Verity/Macro/Translate.lean @@ -516,6 +516,39 @@ private def storageTypeFromSyntax (newtypes : Array NewtypeDecl) (structDecls : } | _ => throwErrorAt stx "invalid struct member declaration" + let rec storageStructMemberElementWords (memberName : String) : ValueType → CommandElabM Nat + | .uint256 | .int256 | .uint16 | .address | .bool | .bytes32 => pure 1 + | .newtype _ baseType => storageStructMemberElementWords memberName baseType + | .fixedArray elemTy size => do + let elemWords ← storageStructMemberElementWords memberName elemTy + pure (elemWords * size) + | other => + throwErrorAt ty + s!"mapping struct member '{memberName}' has unsupported storage type {repr other}; expected a word-like type or FixedArray of word-like types" + + let rec expandStructMemberDecl (memberPrefix : String) (baseOffset : Nat) (memberTy : ValueType) + (packed : Option (Nat × Nat)) : CommandElabM (List StructMemberDecl) := do + match memberTy with + | .newtype _ baseType => + expandStructMemberDecl memberPrefix baseOffset baseType packed + | .fixedArray elemTy size => do + if packed.isSome then + throwErrorAt ty s!"mapping struct fixed-array member '{memberPrefix}' cannot be packed" + let elemWords ← storageStructMemberElementWords memberPrefix elemTy + let nested ← (List.range size).mapM fun idx => + expandStructMemberDecl s!"{memberPrefix}[{idx}]" (baseOffset + idx * elemWords) elemTy none + pure nested.flatten + | .uint256 | .int256 | .uint16 | .address | .bool | .bytes32 => + pure [{ name := memberPrefix, ty := memberTy, wordOffset := baseOffset, packed := packed }] + | other => + throwErrorAt ty + s!"mapping struct member '{memberPrefix}' has unsupported storage type {repr other}; expected a word-like type or FixedArray of word-like types" + + let expandStructMembers (members : Array StructMemberDecl) : CommandElabM (List StructMemberDecl) := do + let expanded ← members.mapM fun member => + expandStructMemberDecl member.name member.wordOffset member.ty member.packed + pure expanded.toList.flatten + let storageArrayElemTypeFromValueType (elemTy : ValueType) : CommandElabM Compiler.CompilationModel.StorageArrayElemType := match elemTy with | .uint256 => pure .uint256 @@ -543,12 +576,12 @@ private def storageTypeFromSyntax (newtypes : Array NewtypeDecl) (structDecls : | `(term| MappingStruct($keyTy:term,[ $[$members:verityStructMember],* ])) => pure <| .mappingStruct (← keyTypeFromSyntax keyTy) - ((← members.mapM structMemberFromSyntax).toList) + (← expandStructMembers (← members.mapM structMemberFromSyntax)) | `(term| MappingStruct2($outerKey:term,$innerKey:term,[ $[$members:verityStructMember],* ])) => pure <| .mappingStruct2 (← keyTypeFromSyntax outerKey) (← keyTypeFromSyntax innerKey) - ((← members.mapM structMemberFromSyntax).toList) + (← expandStructMembers (← members.mapM structMemberFromSyntax)) | _ => do let vt ← valueTypeFromSyntax newtypes structDecls adtDecls ty match vt with diff --git a/artifacts/macro_property_tests/PropertyCreate2SSTORE2Smoke.t.sol b/artifacts/macro_property_tests/PropertyCreate2SSTORE2Smoke.t.sol new file mode 100644 index 000000000..77829454f --- /dev/null +++ b/artifacts/macro_property_tests/PropertyCreate2SSTORE2Smoke.t.sol @@ -0,0 +1,26 @@ +// SPDX-License-Identifier: MIT +pragma solidity ^0.8.33; + +import "./yul/YulTestBase.sol"; + +/** + * @title PropertyCreate2SSTORE2SmokeTest + * @notice Auto-generated baseline property stubs from `verity_contract` declarations. + * @dev Source: Contracts/Smoke.lean + */ +contract PropertyCreate2SSTORE2SmokeTest is YulTestBase { + address target; + address alice = address(0x1111); + + function setUp() public { + target = deployYul("Create2SSTORE2Smoke"); + require(target != address(0), "Deploy failed"); + } + + // Property 1: readCode has no unexpected revert + function testAuto_ReadCode_NoUnexpectedRevert() public { + vm.prank(alice); + (bool ok,) = target.call(abi.encodeWithSignature("readCode(address,uint256,uint256,uint256)", alice, uint256(1), uint256(1), uint256(1))); + require(ok, "readCode reverted unexpectedly"); + } +} diff --git a/artifacts/macro_property_tests/PropertyFixedArrayMappingStructSmoke.t.sol b/artifacts/macro_property_tests/PropertyFixedArrayMappingStructSmoke.t.sol new file mode 100644 index 000000000..11ac73a25 --- /dev/null +++ b/artifacts/macro_property_tests/PropertyFixedArrayMappingStructSmoke.t.sol @@ -0,0 +1,50 @@ +// SPDX-License-Identifier: MIT +pragma solidity ^0.8.33; + +import "./yul/YulTestBase.sol"; + +/** + * @title PropertyFixedArrayMappingStructSmokeTest + * @notice Auto-generated baseline property stubs from `verity_contract` declarations. + * @dev Source: Contracts/Smoke.lean + */ +contract PropertyFixedArrayMappingStructSmokeTest is YulTestBase { + address target; + address alice = address(0x1111); + + function setUp() public { + target = deployYul("FixedArrayMappingStructSmoke"); + require(target != address(0), "Deploy failed"); + } + + // Property 1: writeRoot1 has no unexpected revert + function testAuto_WriteRoot1_NoUnexpectedRevert() public { + vm.prank(alice); + (bool ok,) = target.call(abi.encodeWithSignature("writeRoot1(uint256,bytes32)", uint256(1), bytes32(uint256(0xBEEF)))); + require(ok, "writeRoot1 reverted unexpectedly"); + } + // Property 2: TODO decode and assert `readRoot1` result + function testTODO_ReadRoot1_DecodeAndAssert() public { + vm.prank(alice); + (bool ok, bytes memory ret) = target.call(abi.encodeWithSignature("readRoot1(uint256)", uint256(1))); + require(ok, "readRoot1 reverted unexpectedly"); + assertEq(ret.length, 32, "readRoot1 ABI return length mismatch (expected 32 bytes)"); + // TODO(#1011): decode `ret` and assert the concrete postcondition from Lean theorem. + ret; + } + // Property 3: writeProof11 has no unexpected revert + function testAuto_WriteProof11_NoUnexpectedRevert() public { + vm.prank(alice); + (bool ok,) = target.call(abi.encodeWithSignature("writeProof11(uint256,uint256)", uint256(1), uint256(1))); + require(ok, "writeProof11 reverted unexpectedly"); + } + // Property 4: TODO decode and assert `readProof11` result + function testTODO_ReadProof11_DecodeAndAssert() public { + vm.prank(alice); + (bool ok, bytes memory ret) = target.call(abi.encodeWithSignature("readProof11(uint256)", uint256(1))); + require(ok, "readProof11 reverted unexpectedly"); + assertEq(ret.length, 32, "readProof11 ABI return length mismatch (expected 32 bytes)"); + // TODO(#1011): decode `ret` and assert the concrete postcondition from Lean theorem. + ret; + } +} diff --git a/artifacts/macro_property_tests/PropertyInternalVisibilitySmoke.t.sol b/artifacts/macro_property_tests/PropertyInternalVisibilitySmoke.t.sol new file mode 100644 index 000000000..f1178df30 --- /dev/null +++ b/artifacts/macro_property_tests/PropertyInternalVisibilitySmoke.t.sol @@ -0,0 +1,20 @@ +// SPDX-License-Identifier: MIT +pragma solidity ^0.8.33; + +import "./yul/YulTestBase.sol"; + +/** + * @title PropertyInternalVisibilitySmokeTest + * @notice Auto-generated baseline property stubs from `verity_contract` declarations. + * @dev Source: Contracts/InternalVisibilitySmoke.lean + */ +contract PropertyInternalVisibilitySmokeTest is YulTestBase { + address target; + address alice = address(0x1111); + + function setUp() public { + target = deployYul("InternalVisibilitySmoke"); + require(target != address(0), "Deploy failed"); + } + +} diff --git a/docs/EXTERNAL_CALL_MODULES.md b/docs/EXTERNAL_CALL_MODULES.md index 847030548..28266f183 100644 --- a/docs/EXTERNAL_CALL_MODULES.md +++ b/docs/EXTERNAL_CALL_MODULES.md @@ -115,6 +115,8 @@ Standard modules ship in `Compiler/Modules/`: | `Calls.callWithValue` | Parameterized | Generic `call{value:v}` over an already prepared calldata slice, with revert bubbling | `generic_call_with_value_interface` | | `Calls.callWithValueBytes` | Parameterized | Generic `call{value:v}` over a `bytes` parameter, with revert bubbling | `generic_call_with_value_interface` | | `Calls.bubblingValueCall` / `Calls.bubblingValueCallNoOutput` | `call{value: v}(data)` shape | Generic low-level value call over caller-provided memory slices; bubbles exact revert returndata on failure | `generic_low_level_value_call_interface` | +| `Create2SSTORE2.create2Deploy` | Parameterized | `create2(value, offset, size, salt)` over caller-prepared initcode | `create2_initcode_layout`, `create2_address_derivation` | +| `Create2SSTORE2.sstore2ReadCode` | Parameterized | `extcodecopy(pointer, dest, codeOffset, size)` for code-as-data reads | `sstore2_pointer_code_layout` | See `Compiler/Modules/README.md` for the full checklist on adding new standard modules. @@ -190,6 +192,19 @@ the selector/static-argument/dynamic-bytes ABI layout, while the callback target's protocol-specific behavior remains the `callback_target_interface` assumption. +### CREATE2 and SSTORE2 Helpers + +`Compiler.Modules.Create2SSTORE2.deployModule` lowers a source-level ECM call to +`create2(value, offset, size, salt)` and binds the returned address word. The +caller is responsible for preparing the initcode memory slice and for proving or +assuming the `create2_initcode_layout` and `create2_address_derivation` +boundaries. + +`Compiler.Modules.Create2SSTORE2.readCodeModule` lowers to `extcodecopy` for +SSTORE2-style code-as-data reads. The helper only models the copy mechanic; the +meaning of the pointer's bytecode layout remains the +`sstore2_pointer_code_layout` assumption. + ### Packed Hashing Helpers `Compiler.Modules.Hashing.abiEncodeStaticWords` covers diff --git a/scripts/check_macro_property_test_generation.py b/scripts/check_macro_property_test_generation.py index a458223af..afb8030ec 100644 --- a/scripts/check_macro_property_test_generation.py +++ b/scripts/check_macro_property_test_generation.py @@ -40,6 +40,10 @@ # Lean macro/model tests; the standalone compiler artifact path does not # materialize these ECM smoke contracts for generated no-revert stubs. "PackedHashECMSmoke", + # Callback target behavior is intentionally external; generated no-revert + # fuzz stubs can pick a target that reverts. Lean/Yul/trust-report tests + # cover the ABI layout. + "CallbackABISmoke", } From b229623bbeeff0935874edf1ac1f026983e6dbc5 Mon Sep 17 00:00:00 2001 From: Thomas Marchand Date: Wed, 3 Jun 2026 16:11:56 +0200 Subject: [PATCH 3/4] Drop redundant internal visibility macro --- Contracts/InternalVisibilitySmoke.lean | 46 ------------------- Verity/Macro/Syntax.lean | 1 - Verity/Macro/Translate.lean | 16 +------ .../PropertyInternalVisibilitySmoke.t.sol | 20 -------- 4 files changed, 2 insertions(+), 81 deletions(-) delete mode 100644 Contracts/InternalVisibilitySmoke.lean delete mode 100644 artifacts/macro_property_tests/PropertyInternalVisibilitySmoke.t.sol diff --git a/Contracts/InternalVisibilitySmoke.lean b/Contracts/InternalVisibilitySmoke.lean deleted file mode 100644 index bc0a53e13..000000000 --- a/Contracts/InternalVisibilitySmoke.lean +++ /dev/null @@ -1,46 +0,0 @@ -import Compiler.CheckContract -import Compiler.Selector -import Verity.Macro - -namespace Contracts.InternalVisibilitySmoke - -open Verity hiding pure bind -open Verity.EVM.Uint256 - -verity_contract InternalVisibilitySmoke where - storage - value : Uint256 := slot 0 - - function internal pure double (x : Uint256) : Uint256 := do - return add x x - - function view readDouble (x : Uint256) : Uint256 := do - let y ← double x - return y - - function view read () : Uint256 := do - let current ← getStorage value - return current - -example : - (InternalVisibilitySmoke.spec.functions.map (fun fn => (fn.name, fn.isInternal))) = - [ ("internal_double", true) - , ("readDouble", false) - , ("read", false) - , ("internal_readDouble", true) - , ("internal_read", true) - ] := by decide - -example : - (InternalVisibilitySmoke.spec.functions.filter - (fun fn => !fn.isInternal && !Compiler.CompilationModel.isInteropEntrypointName fn.name) - |>.map (·.name)) = - ["readDouble", "read"] := by decide - -example : - InternalVisibilitySmoke.spec.functions.all (fun fn => - fn.isInternal || fn.name == "readDouble" || fn.name == "read") = true := by decide - -#check_contract InternalVisibilitySmoke - -end Contracts.InternalVisibilitySmoke diff --git a/Verity/Macro/Syntax.lean b/Verity/Macro/Syntax.lean index 23e7efd98..6f99ad104 100644 --- a/Verity/Macro/Syntax.lean +++ b/Verity/Macro/Syntax.lean @@ -70,7 +70,6 @@ syntax "external " ident "(" sepBy(term, ",") ")" " -> " "(" sepBy(term, ",") ") syntax ident " := " ident ppSpace str : verityLocalObligation syntax "local_obligations " "[" sepBy(verityLocalObligation, ",") "]" : verityLocalObligations syntax "payable" : verityMutability -syntax "internal" : verityMutability syntax "view" : verityMutability syntax pureMutabilityMarker := &"pure" syntax "no_external_calls" : verityMutability diff --git a/Verity/Macro/Translate.lean b/Verity/Macro/Translate.lean index d8255f69c..4189262fc 100644 --- a/Verity/Macro/Translate.lean +++ b/Verity/Macro/Translate.lean @@ -210,8 +210,6 @@ structure FunctionDecl where params : Array ParamDecl returnTy : ValueType isPayable : Bool := false - /-- Whether this function is internal-only and excluded from selector dispatch/ABI. -/ - isInternal : Bool := false isView : Bool := false isPure : Bool := false noExternalCalls : Bool := false @@ -1183,7 +1181,6 @@ private def parseLocalObligation (stx : Syntax) : CommandElabM LocalObligationDe private structure ParsedMutability where isPayable : Bool := false - isInternal : Bool := false isView : Bool := false isPure : Bool := false noExternalCalls : Bool := false @@ -1201,10 +1198,6 @@ private def parseMutabilityModifiers if result.isPayable then throwErrorAt mod "duplicate 'payable' modifier" result := { result with isPayable := true } - | `(verityMutability| internal) => - if result.isInternal then - throwErrorAt mod "duplicate 'internal' modifier" - result := { result with isInternal := true } | `(verityMutability| view) => if result.isView then throwErrorAt mod "duplicate 'view' modifier" @@ -1344,7 +1337,6 @@ private def parseFunction (newtypes : Array NewtypeDecl) (structDecls : Array St params := parsedParams returnTy := parsedReturnTy isPayable := mut_.isPayable - isInternal := mut_.isInternal isView := mut_.isView isPure := mut_.isPure noExternalCalls := mut_.noExternalCalls @@ -7951,7 +7943,7 @@ private def mkSpecCommand }) let functionModelIds ← functions.mapM fun fn => mkSuffixedIdent fn.ident "_model" let internalFunctionTerms ← functions.filterMapM fun fn => do - if supportsInternalHelperSpec fn && !fn.isInternal then + if supportsInternalHelperSpec fn then let modelBodyName ← mkSuffixedIdent fn.ident "_modelBody" let modelParams ← mkModelParamsTerm fn.params let localObligationTerms ← fn.localObligations.mapM mkModelLocalObligationTerm @@ -8621,7 +8613,6 @@ def mkFunctionCommandsPublic let modelParams ← mkModelParamsTerm fn.params let localObligationTerms ← fn.localObligations.mapM mkModelLocalObligationTerm let payableTerm ← if fn.isPayable then `(true) else `(false) - let internalTerm ← if fn.isInternal then `(true) else `(false) let viewTerm ← if fn.isView then `(true) else `(false) let pureTerm ← if fn.isPure then `(true) else `(false) let noExternalCallsTerm ← if fn.noExternalCalls then `(true) else `(false) @@ -8636,13 +8627,11 @@ def mkFunctionCommandsPublic let modifiesTerms : Array Term := fn.modifies.map fun ident => strTerm (toString ident.getId) let returnTypeTerm ← modelReturnTypeTerm fn.returnTy let returnsTerm ← modelReturnsTerm fn.returnTy - let modelSpecName := - if fn.isInternal then internalHelperSpecNameFor fn else fn.name let fnCmd : Cmd ← `(command| def $fn.ident : $fnType := $fnValue) let bodyCmd : Cmd ← `(command| def $modelBodyName : List Compiler.CompilationModel.Stmt := [ $[$stmtTerms],* ]) let modelCmd : Cmd ← `(command| def $modelName : Compiler.CompilationModel.FunctionSpec := { - name := $(strTerm modelSpecName) + name := $(strTerm fn.name) params := $modelParams returnType := $returnTypeTerm «returns» := $returnsTerm @@ -8657,7 +8646,6 @@ def mkFunctionCommandsPublic modifies := [ $[$modifiesTerms],* ] localObligations := [ $[$localObligationTerms],* ] body := $modelBodyName - isInternal := $internalTerm }) pure #[fnCmd, bodyCmd, modelCmd] diff --git a/artifacts/macro_property_tests/PropertyInternalVisibilitySmoke.t.sol b/artifacts/macro_property_tests/PropertyInternalVisibilitySmoke.t.sol deleted file mode 100644 index f1178df30..000000000 --- a/artifacts/macro_property_tests/PropertyInternalVisibilitySmoke.t.sol +++ /dev/null @@ -1,20 +0,0 @@ -// SPDX-License-Identifier: MIT -pragma solidity ^0.8.33; - -import "./yul/YulTestBase.sol"; - -/** - * @title PropertyInternalVisibilitySmokeTest - * @notice Auto-generated baseline property stubs from `verity_contract` declarations. - * @dev Source: Contracts/InternalVisibilitySmoke.lean - */ -contract PropertyInternalVisibilitySmokeTest is YulTestBase { - address target; - address alice = address(0x1111); - - function setUp() public { - target = deployYul("InternalVisibilitySmoke"); - require(target != address(0), "Deploy failed"); - } - -} From 7f69bda585d44478664c4a8d4da1a8965387a55d Mon Sep 17 00:00:00 2001 From: Thomas Marchand Date: Wed, 3 Jun 2026 16:50:50 +0200 Subject: [PATCH 4/4] Document Verity architecture surfaces --- README.md | 1 + docs-site/content/architecture.mdx | 71 ++++++++++++++++++ docs-site/content/capabilities.mdx | 5 +- docs-site/content/compiler.mdx | 6 +- docs/ARCHITECTURE.md | 112 +++++++++++++++++++++++++++++ 5 files changed, 191 insertions(+), 4 deletions(-) create mode 100644 docs/ARCHITECTURE.md diff --git a/README.md b/README.md index 5ac81c858..ea83f9450 100644 --- a/README.md +++ b/README.md @@ -93,6 +93,7 @@ Verity is complementary to these tools. It is for cases where you need mathemati | [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 | diff --git a/docs-site/content/architecture.mdx b/docs-site/content/architecture.mdx index 0fa8ee1ae..db2f54220 100644 --- a/docs-site/content/architecture.mdx +++ b/docs-site/content/architecture.mdx @@ -46,6 +46,44 @@ by `solc --strict-assembly`. Layer 3 proves IR-to-Yul preservation. `forge create` or any standard toolchain. Not in the proof envelope: `solc`, the EVM spec, and the deployment pipeline are explicit trust assumptions. +## Layer responsibilities + +When a Solidity-facing feature is added, the first question is which layer should +own it. Verity works best when features enter as typed source or +`CompilationModel` constructs and the later layers only lower those constructs. + +| Feature kind | Owner | Examples | +|---|---|---| +| Source syntax | `verity_contract` macro | Source-level `internal` helpers, storage declarations, function parameters | +| Storage layout | Macro lowering plus `CompilationModel` fields | `MappingStruct`, `MappingStruct2`, fixed-array struct members like `roots[1]` | +| Reusable call mechanics | External Call Modules | ERC-20 wrappers, callbacks, CREATE2, SSTORE2, precompiles | +| Low-level primitives | `CompilationModel` mechanics and trust reports | `call`, `staticcall`, `create2`, `extcodecopy`, `codecopy` | +| Proof widening | Supported-fragment witnesses and theorem interfaces | Moving a feature from tested/trusted into the proved subset | + +That split matters because many production ports need Solidity mechanisms that +are useful but not yet fully modeled in the proof interpreters. Verity should +still compile them directly, but it must also surface the remaining assumption in +`--trust-report` and let proof-strict users reject it with a `--deny-*` flag. + +## Source-to-model boundary + +The `verity_contract` macro emits both executable Lean code and a +`CompilationModel`. The model is the compiler-facing artifact; selector dispatch, +ABI output, storage layout, trust reporting, and later proofs all consume it. + +Source-level `internal` functions become internal `FunctionSpec`s. They are +callable by other functions, but they are excluded from external selector +dispatch and ABI JSON. Entry points with special EVM meaning, such as `receive` +and `fallback`, should stay external because their names control dispatcher +behavior. + +Storage declarations lower to typed fields. For Morpho-style mapping structs, +fixed-array members are expanded into stable word-addressed member names. A +declaration such as `roots : FixedArray Bytes32 2 @word 1` exposes +`roots[0]` and `roots[1]`; nested arrays expose names like `proof[1][1]`. +Those names are what `structMember` and `setStructMember` use, so generated Yul +remains layout-reviewable without post-processing. + ## Source layer (EDSL) The EDSL is intentionally small. `Verity/Core.lean` provides `ContractState`, @@ -68,6 +106,25 @@ linker (`Compiler/Linker.lean`) splices external Yul libraries (`--link`) into the runtime section after validating that every non-builtin call site is satisfied. See [`/compiler`](/compiler) for the pipeline narrative and CLI reference. +## Extension surfaces + +External Call Modules, or ECMs, are the standard extension point for reusable +low-level or protocol-shaped calls. An ECM packages argument count, result +bindings, mutability flags, Yul lowering, and named assumptions. Standard ECMs +ship for ERC-20 optional-return wrappers, ERC-4626 reads, callbacks, generic +value calls, precompiles, CREATE2 deployment, and SSTORE2-style code-as-data +reads. + +The CREATE2/SSTORE2 helpers are a representative boundary: + +- `create2Deploy` lowers `create2(value, offset, size, salt)` and records + `create2_initcode_layout` plus `create2_address_derivation`. +- `sstore2ReadCode` lowers `extcodecopy(pointer, dest, codeOffset, size)` and + records `sstore2_pointer_code_layout`. + +The compiler owns the mechanics. The contract or protocol proof owns the meaning +of the initcode, derived address, pointer bytecode layout, and returned data. + ## Verification layers (1 / 2 / 3) The pipeline annotations above mark where each layer applies — Layer 1 (EDSL → @@ -76,6 +133,20 @@ canonical breakdown of what is proved versus trusted at each layer lives on the [Trust Model](/trust-model), and the live theorem inventory is on [Verification Status](/verification). +## Contributor checklist + +For a new production feature, check every row before relying on it: + +| Area | Question | +|---|---| +| Source surface | Is there source syntax or a documented API, with validation errors for unsupported forms? | +| Model surface | Does it lower to typed `CompilationModel` data rather than patched generated Yul? | +| ABI and dispatch | Are external selectors, ABI output, internal helpers, `receive`, and `fallback` handled explicitly? | +| Storage layout | Are resolved names, offsets, packing, and aliases visible in the model and layout report? | +| Trust reporting | Do all unproved mechanics appear in `--trust-report`, with fail-closed deny flags when appropriate? | +| Tests | Is there source smoke coverage, lowering coverage, trust-report coverage, and generated property-test coverage where relevant? | +| Docs | Do the architecture, capabilities, and feature-specific docs describe both behavior and remaining trust boundary? | + ## Where to next - [Getting Started](/getting-started), local setup and your first verification run. diff --git a/docs-site/content/capabilities.mdx b/docs-site/content/capabilities.mdx index 9004925bd..593386847 100644 --- a/docs-site/content/capabilities.mdx +++ b/docs-site/content/capabilities.mdx @@ -22,7 +22,7 @@ Modeled end-to-end and carrying machine-checked preservation proofs. **Storage** - Scalar fields (`Uint256`, `Address`, `Bool`, `Bytes32`) at explicit slots. - Mappings: single, double (`mapping2`), `Uint256`-keyed, plus word-offset and packed-word variants. -- Structs and nested storage structs; struct-valued mappings (`MappingStruct`). +- Structs and nested storage structs; struct-valued mappings (`MappingStruct` / `MappingStruct2`), including fixed-array members expanded into stable names such as `roots[1]` and `proof[1][1]`. - ERC-7201 namespaced storage roots (`storage_namespace`). **Computation** @@ -37,6 +37,7 @@ Modeled end-to-end and carrying machine-checked preservation proofs. - Context reads: `msgSender` (`caller`), `msg.value`, `block.timestamp`. **Composition** +- Source-level `internal` functions. They compile as internal helpers, stay out of selector dispatch and ABI JSON, and are called by direct helper syntax. - Internal helpers called directly by name. A helper's proved summary is now reusable at every call site through the first-class interface `SupportedSpecHelperProofs.helperCallSummarySound` and its three call-shape corollaries (`evalInternalCallObeysSummary`, `execInternalCallObeysSummary`, `execInternalCallAssignObeysSummary`) covering expression position, void statement, and assigning statement ([#1630](https://github.com/lfglabs-dev/verity/issues/1630)). - Higher-order internal calls (function-pointer parameters) via a compile-time monomorphization pre-pass; nothing downstream of the parser sees a function pointer, so the first-order proof machinery applies unchanged ([#1747](https://github.com/lfglabs-dev/verity/issues/1747)). - External linked libraries (`externalCall`) — sound **by assumption** on the linked Yul (e.g. Poseidon), recorded as a trust surface. @@ -60,7 +61,7 @@ Compiler-supported and differential-tested, but only partially covered by the pr These lower to correct Yul and are differential-tested, but are **not modeled in the proof interpreters**. Treat each as an explicit trust assumption and archive `--trust-report`. - **`keccak256`** — compiles through the typed intrinsic but relies on an explicit hashing axiom. Enforce exclusion with `--deny-axiomatized-primitives`. -- **Low-level calls** — `call` / `staticcall` / `delegatecall`, `externalCallBind`, and ECMs (`callWithValue`, `bubblingValueCall`, SafeTransferLib-style optional-return helpers, and the precompile ECMs `ecrecover` / `sha256` / BN254). Enforce with `--deny-low-level-mechanics`; each precompile carries its own `evm_*_precompile` axiom (see [`docs/EXTERNAL_CALL_MODULES.md`](https://github.com/lfglabs-dev/verity/blob/main/docs/EXTERNAL_CALL_MODULES.md) and [`AXIOMS.md`](https://github.com/lfglabs-dev/verity/blob/main/AXIOMS.md)). +- **Low-level calls and code mechanics** — `call` / `staticcall` / `delegatecall`, `externalCallBind`, CREATE2, SSTORE2-style `extcodecopy`, and ECMs (`callWithValue`, `bubblingValueCall`, SafeTransferLib-style optional-return helpers, callback helpers, CREATE2/SSTORE2 helpers, and the precompile ECMs `ecrecover` / `sha256` / BN254). Enforce with `--deny-low-level-mechanics`; each precompile carries its own `evm_*_precompile` axiom, while CREATE2/SSTORE2 carry layout/address assumptions (see [`docs/EXTERNAL_CALL_MODULES.md`](https://github.com/lfglabs-dev/verity/blob/main/docs/EXTERNAL_CALL_MODULES.md) and [`AXIOMS.md`](https://github.com/lfglabs-dev/verity/blob/main/AXIOMS.md)). - **`delegatecall` / proxy upgradeability** — additionally a dedicated proxy boundary. Enforce with `--deny-proxy-upgradeability` ([#1420](https://github.com/lfglabs-dev/verity/issues/1420)). - **Raw payload plumbing** — `calldatacopy`, `returndataCopy`, `revertReturndata`, `rawLog`. Enforce raw-event exclusion with `--deny-event-emission` and low-level mechanics with `--deny-low-level-mechanics`. - **Nested dynamic ABI decoding** — `arrayElementDynamicWord` / `paramDynamicHeadWord` read checked static leaf words from dynamic-shaped calldata; the decoded words are not modeled in proofs ([#1832](https://github.com/lfglabs-dev/verity/issues/1832)). diff --git a/docs-site/content/compiler.mdx b/docs-site/content/compiler.mdx index 6230efa39..364fab78b 100644 --- a/docs-site/content/compiler.mdx +++ b/docs-site/content/compiler.mdx @@ -173,9 +173,11 @@ constructor := some { Low-level calls, linear-memory primitives, `rawLog`, and `keccak256` compile but remain partially modeled in the proof interpreter. Functions or constructors that use direct assembly-shaped primitives without `localObligations [...]` fail closed under `--deny-axiomatized-primitives`. -**Internal functions** via `isInternal := true` and `Stmt.internalCall`; **events** via the `events` block and `Stmt.emit` (topic0 computed from the event signature); **double mappings** via `mapping2`/`setMapping2`; **uint256-keyed mappings**; **dynamic, fixed-size, and bytes array parameters** with ABI offset-based decoding. +**Internal functions** via source-level `internal` helpers, `isInternal := true`, and `Stmt.internalCall`; these helpers stay out of selector dispatch and ABI JSON. **Events** via the `events` block and `Stmt.emit` (topic0 computed from the event signature); **double mappings** via `mapping2`/`setMapping2`; **uint256-keyed mappings**; **dynamic, fixed-size, and bytes array parameters** with ABI offset-based decoding. -For Morpho-style `mapping(K => Struct)` / `mapping(K1 => mapping(K2 => Struct))` layouts, declare `FieldType.mappingStruct` / `FieldType.mappingStruct2` and access members with `structMember "f" key "member"`, `structMember2 "f" k1 k2 "member"`, `setStructMember "f" key "member" val`, `setStructMember2 "f" k1 k2 "member" val`. Member offsets and packed sub-word widths are part of the declaration, so the generated Yul keeps Solidity's storage layout reviewable. +For Morpho-style `mapping(K => Struct)` / `mapping(K1 => mapping(K2 => Struct))` layouts, declare `FieldType.mappingStruct` / `FieldType.mappingStruct2` and access members with `structMember "f" key "member"`, `structMember2 "f" k1 k2 "member"`, `setStructMember "f" key "member" val`, `setStructMember2 "f" k1 k2 "member" val`. Member offsets and packed sub-word widths are part of the declaration, so the generated Yul keeps Solidity's storage layout reviewable. Fixed-array members are expanded at elaboration time into stable member names such as `roots[1]` and `proof[1][1]`. + +Standard External Call Modules cover reusable low-level call patterns without generated-Yul patching. The CREATE2/SSTORE2 module lowers source-level `create2Deploy` and `sstore2ReadCode` ECM calls to `create2` and `extcodecopy`, while trust reports keep the initcode layout, address derivation, and pointer bytecode layout assumptions visible. ## Add a new contract diff --git a/docs/ARCHITECTURE.md b/docs/ARCHITECTURE.md new file mode 100644 index 000000000..162a65af6 --- /dev/null +++ b/docs/ARCHITECTURE.md @@ -0,0 +1,112 @@ +# Verity Architecture + +This page is the contributor map for Verity. It explains where a source feature +enters the system, which proof layer owns it, which trust report should mention +it, and which tests should fail if it regresses. + +For the public user-facing version, see `docs-site/content/architecture.mdx`. + +## End-to-end Pipeline + +``` +verity_contract source + -> macro elaboration + -> CompilationModel + -> IR + -> Yul + -> solc bytecode +``` + +| Stage | Main files | Owns | Proof / trust status | +|---|---|---|---| +| Source syntax and executable semantics | `Verity/Core.lean`, `Verity/Macro/*` | Contract author surface, storage declarations, source helpers, executable Lean definitions | Layer 1 bridge to `CompilationModel` for the macro-lowered image | +| CompilationModel | `Compiler/CompilationModel/*`, `Compiler/ECM.lean` | Declarative storage, params, statements, events, externals, ECMs, validation, trust-surface collection | Layer 2 source of truth; full model is wider than the proved fragment | +| IR generation and proofs | `Compiler/Proofs/IRGeneration/*` | Supported-fragment witnesses, helper-aware source semantics, IR lowering correctness | Generic Layer 2 theorem for `SupportedSpec` | +| Yul generation and native bridge | `Compiler/Codegen.lean`, `Compiler/Yul/*`, `Compiler/Proofs/YulGeneration/*` | Dispatcher, ABI loads, runtime helpers, Yul AST, EVMYulLean bridge | Layer 3 theorem surface plus explicit bridge hypotheses | +| Bytecode | `solc --strict-assembly` | Final deployment artifact | Trusted boundary | + +## Feature Ownership + +When adding a source-level feature, update the earliest layer that can express it +precisely. Avoid patching generated Yul when the feature has a stable source or +`CompilationModel` shape. + +| Feature kind | Preferred owner | Examples | Notes | +|---|---|---|---| +| Ordinary source syntax | `Verity/Macro/*` | `internal` source helpers, storage declarations, ABI-shaped parameters | Must lower to ordinary `CompilationModel` fields/functions/statements. | +| Storage layout metadata | `Verity/Macro/Translate.lean` plus `Compiler/CompilationModel/*` | `MappingStruct`, `MappingStruct2`, fixed arrays inside struct members | Keep layout reviewable as named members and word offsets. | +| Reusable low-level mechanics | `Compiler/Modules/*` ECMs | ERC-20 wrappers, callbacks, CREATE2, SSTORE2, precompiles | Must expose assumptions through `axioms` and trust reports. | +| New primitive mechanic | `Compiler/CompilationModel/Types.lean` and `TrustSurface.lean` | `create2`, `extcodecopy`, `codecopy` | Add deny-flag classification when the mechanic is outside the proof model. | +| Proof-complete widening | `Compiler/Proofs/*` and boundary catalogs | New supported statement forms, helper-summary consumption | Do not claim full verification until the relevant theorem consumes the feature. | + +## Current Solidity-facing Surfaces + +These are easy to miss because they cross source syntax, layout, ECMs, and trust +reporting: + +- Source-level `internal` functions compile as internal helper specs, are omitted + from selector dispatch and ABI output, and must not be used for `receive` or + `fallback` entrypoints. +- `MappingStruct` and `MappingStruct2` support fixed-array members by expanding + them into stable member names such as `roots[1]` and `proof[1][1]`. The + expanded names are the storage-access names used by `structMember` and + `setStructMember`. +- Dynamic calldata structs and arrays are represented through the existing ABI + head/element machinery. Do not add one-off Yul rewrites when a typed + parameter-load path already exists. +- Callback helpers live in `Compiler.Modules.Callbacks` and own the ABI frame + for selector plus static arguments plus dynamic bytes. Protocol behavior of + the callback target remains an assumption. +- CREATE2 and SSTORE2-style code-as-data live in + `Compiler.Modules.Create2SSTORE2`. They lower the mechanics directly and + surface `create2_initcode_layout`, `create2_address_derivation`, and + `sstore2_pointer_code_layout` as assumptions. + +## Trust-surface Rule + +If a feature uses a mechanic that the proof interpreters do not fully model, the +compiler must make that visible. + +1. Add a typed `CompilationModel` or ECM representation. +2. Classify the mechanic in `Compiler/CompilationModel/TrustSurface.lean`. +3. Add or update `--deny-*` behavior if users need a fail-closed gate. +4. Add a trust-report test in `Compiler/CompilationModelFeatureTest.lean`. +5. Document the assumption in `docs/EXTERNAL_CALL_MODULES.md`, + `docs/VERIFICATION_STATUS.md`, `docs-site/content/capabilities.mdx`, or the + narrower page for that surface. + +## Checklist for New Solidity-facing Features + +- Source syntax or API documented in `docs-site/content/edsl-api-reference.mdx` + or the relevant guide. +- `CompilationModel` representation is typed and validated; no ad hoc string + patching of generated Yul. +- Mutability, selector/ABI visibility, result binding, and local scoping are + tested. +- Storage layout changes have smoke tests for resolved names and word offsets. +- Low-level mechanics are collected by `TrustSurface.lean` and covered by + `--trust-report`. +- ECMs list every protocol assumption in `axioms`. +- `Compiler/CompilationModelFeatureTest.lean` covers lowering and reports. +- `Contracts/Smoke.lean` or a focused contract test covers the source surface. +- Generated Foundry/property fixtures are updated when the macro property + surface changes. +- Public docs and repo docs name both the supported behavior and the remaining + proof/trust boundary. + +## Fast Regression Commands + +Use focused checks before the full suite: + +```bash +lake build Verity.Macro.Translate +lake build Contracts.Smoke Compiler.CompilationModelFeatureTest +python3 scripts/check_macro_property_test_generation.py +python3 scripts/check_trust_surface_registry.py +``` + +Then run the full gate: + +```bash +make check +```