From a5ca06acd3a9d2d2f12da4aba60624a017e14f3a Mon Sep 17 00:00:00 2001 From: Thomas Marchand Date: Sun, 6 Sep 2026 23:51:50 +0000 Subject: [PATCH 01/50] feat(macro): generate adversary-indexed entrypoint registry --- .../ReentrancyRelyGuarantee/Contract.lean | 4 +- Verity/Core/Model/CallbackBridge.lean | 26 +++++++-- Verity/Macro/Elaborate.lean | 2 + Verity/Macro/Translate.lean | 55 ++++++++++++++++++- 4 files changed, 78 insertions(+), 9 deletions(-) diff --git a/Contracts/ReentrancyRelyGuarantee/Contract.lean b/Contracts/ReentrancyRelyGuarantee/Contract.lean index 1ca927351..9871914cc 100644 --- a/Contracts/ReentrancyRelyGuarantee/Contract.lean +++ b/Contracts/ReentrancyRelyGuarantee/Contract.lean @@ -203,7 +203,7 @@ any `CallProgram`, and through the transaction commit/revert boundary. -/ open Compiler.CompilationModel.DenoteExternalCalls in theorem callback_bounded_program_preserves_I {adversary : AdversaryModel} - (hbound : CallbackBounded spec.entrypoints adversary) + (hbound : CallbackBounded (EntrypointRegistry.ofList spec.entrypoints) adversary) (prog : CallProgram α) (state : CallState) (hInv : I state.world) : I (denote prog adversary state).2.world := hbound.denote_preserves spec prog state hInv @@ -211,7 +211,7 @@ theorem callback_bounded_program_preserves_I open Compiler.CompilationModel.DenoteExternalCalls in theorem callback_bounded_transaction_preserves_I {adversary : AdversaryModel} {α : Type} - (hbound : CallbackBounded spec.entrypoints adversary) + (hbound : CallbackBounded (EntrypointRegistry.ofList spec.entrypoints) adversary) (prog : CallProgram (TransactionResult α)) (state : CallState) (hInv : I state.world) : I (denoteTransaction prog adversary state).state.world := diff --git a/Verity/Core/Model/CallbackBridge.lean b/Verity/Core/Model/CallbackBridge.lean index a262ca253..40f2ba0c0 100644 --- a/Verity/Core/Model/CallbackBridge.lean +++ b/Verity/Core/Model/CallbackBridge.lean @@ -22,15 +22,31 @@ namespace Compiler.CompilationModel.DenoteExternalCalls open Verity.Core.Invariant (Preserves runSeq) open Verity.Core.Reentrancy (ReentrancySpec) +/-- The macro-emitted registry is a predicate rather than a list of already +applied functions. This keeps entrypoint arguments existential and, crucially, +indexes every executable transition by the same explicit adversary used at the +call boundary. -/ +abbrev EntrypointRegistry := + AdversaryModel → (Verity.ContractState → Verity.ContractState) → Prop + +namespace EntrypointRegistry + +/-- Compatibility adapter for the original, argument-free worked examples. -/ +def ofList (entrypoints : List (Verity.ContractState → Verity.ContractState)) : + EntrypointRegistry := + fun _ entrypoint => entrypoint ∈ entrypoints + +end EntrypointRegistry + /-- Each mutable transition is some finite reentry schedule drawn from the registry. Static sites are unrestricted: `denoteCall` never commits their transitions, and `Conforms` separately pins them externally. -/ def CallbackBounded - (entrypoints : List (Verity.ContractState → Verity.ContractState)) + (entrypoints : EntrypointRegistry) (adversary : AdversaryModel) : Prop := ∀ site world, site.kind ≠ .staticcall → ∃ sched : List (Verity.ContractState → Verity.ContractState), - (∀ f ∈ sched, f ∈ entrypoints) ∧ + (∀ f ∈ sched, entrypoints adversary f) ∧ adversary.stateTransition site world = runSeq sched world /-- One external call under a callback-bounded adversary preserves the spec @@ -38,7 +54,7 @@ invariant: rollback outcomes keep the pre-call world, and committed outcomes are reentry schedules, covered by the per-entrypoint obligations. -/ theorem CallbackBounded.denoteCall_preserves (spec : ReentrancySpec) {adversary : AdversaryModel} - (h : CallbackBounded spec.entrypoints adversary) + (h : CallbackBounded (EntrypointRegistry.ofList spec.entrypoints) adversary) (site : CallSite) (state : CallState) (hInv : spec.Inv state.world) : spec.Inv (denoteCall adversary site state).state.world := by @@ -78,7 +94,7 @@ sequence of externally opened windows — each free to reenter through any registered schedule — can break it. -/ theorem CallbackBounded.denote_preserves (spec : ReentrancySpec) {adversary : AdversaryModel} - (h : CallbackBounded spec.entrypoints adversary) + (h : CallbackBounded (EntrypointRegistry.ofList spec.entrypoints) adversary) (prog : CallProgram α) (state : CallState) (hInv : spec.Inv state.world) : spec.Inv (denote prog adversary state).2.world := by @@ -94,7 +110,7 @@ invariant state by the program law, and a reverted one by rollback to the initial state. -/ theorem CallbackBounded.transaction_preserves (spec : ReentrancySpec) {adversary : AdversaryModel} - (h : CallbackBounded spec.entrypoints adversary) + (h : CallbackBounded (EntrypointRegistry.ofList spec.entrypoints) adversary) (prog : CallProgram (TransactionResult α)) (state : CallState) (hInv : spec.Inv state.world) : spec.Inv (denoteTransaction prog adversary state).state.world := by diff --git a/Verity/Macro/Elaborate.lean b/Verity/Macro/Elaborate.lean index 1a521eade..723755bd4 100644 --- a/Verity/Macro/Elaborate.lean +++ b/Verity/Macro/Elaborate.lean @@ -176,6 +176,8 @@ private def elabVerityContractOrMixin (stx : Syntax) : CommandElabM Unit := do elabCommand cmd elabCommand (← mkBridgeCommand fn.ident) + elabCommand (← mkEntrypointRegistryCommandPublic translationFunctions) + -- Constructors may call internal helpers, so emit them only after the -- executable helper definitions are available in the namespace. if isMixin then diff --git a/Verity/Macro/Translate.lean b/Verity/Macro/Translate.lean index 9e6a4cc96..031ff08a5 100644 --- a/Verity/Macro/Translate.lean +++ b/Verity/Macro/Translate.lean @@ -16,6 +16,8 @@ import Verity.Macro.Internal import Verity.Macro.Storage import Verity.Macro.Types import Verity.Macro.Syntax +import Verity.Core.Model.CallbackBridge +import Verity.Core.Model.NonReentrantGuard namespace Verity.Macro @@ -4960,7 +4962,7 @@ def validateGeneratedDefNamesPublic (constDecls : Array ConstantDecl) (immutableDecls : Array ImmutableDecl) (functions : Array FunctionDecl) : CommandElabM Unit := do - let reservedGeneratedNames : Array String := #["spec", "storageNamespace"] + let reservedGeneratedNames : Array String := #["spec", "storageNamespace", "entrypointRegistry"] let mut generatedHelperNames : Array String := reservedGeneratedNames if hasStructMapping fields then generatedHelperNames := generatedHelperNames.push "structMember" @@ -5542,6 +5544,13 @@ def mkFunctionCommandsPublic mkContractFnType fn.params fn.returnTy let fnExecutableBody := ⟨← threadAdversaryThroughExecutableSyntax externalDecls adversarialHelpers fn.params advTerm fnExecutableBody.raw⟩ + let fnExecutableBody ← match fn.nonReentrantLock with + | some lockIdent => + let lockName := toString lockIdent.getId + let some lockField := fields.find? (fun field => field.name == lockName) + | throwErrorAt lockIdent s!"unknown nonreentrant lock field '{lockName}'" + `(Verity.Core.NonReentrantGuard.guarded $(natTerm lockField.slotNum) $fnExecutableBody) + | none => pure fnExecutableBody let fnValue ← if opensReentrancyWindow then mkContractFnValueWithAdversary advIdent fn.params fnExecutableBody else @@ -5566,6 +5575,32 @@ def mkFunctionCommandsPublic let returnsTerm ← modelReturnsTerm fn.returnTy let fnCmd : Cmd ← `(command| def $fn.ident : $fnType := $fnValue) + let entrypointPredicateName ← mkSuffixedIdent fn.ident "_entrypoint" + let registryAdvIdent ← Lean.Elab.Term.mkFreshIdent + (mkIdentFrom fn.ident `_registryAdv).raw + let transitionIdent ← Lean.Elab.Term.mkFreshIdent + (mkIdentFrom fn.ident `_transition).raw + let mut applied : Term := fn.ident + if opensReentrancyWindow then + applied ← `($applied (ExecutableCallContext.ofAdversary $(⟨registryAdvIdent.raw⟩))) + let mut registryParams : Array (Ident × Term) := #[] + for param in fn.params do + let paramTy ← contractValueTypeTerm param.ty + let paramIdent ← Lean.Elab.Term.mkFreshIdent + (mkIdentFrom param.ident `_registryArg).raw + registryParams := registryParams.push (⟨paramIdent.raw⟩, paramTy) + applied ← `($applied $(⟨paramIdent.raw⟩)) + let mut registryBody : Term ← + `(($(⟨transitionIdent.raw⟩) : Verity.ContractState → Verity.ContractState) = + ($applied).runState) + for (paramIdent, paramTy) in registryParams.reverse do + registryBody ← `(∃ $paramIdent : $paramTy, $registryBody) + let entrypointCmd : Cmd ← `(command| + def $entrypointPredicateName + ($(⟨registryAdvIdent.raw⟩) : + Compiler.CompilationModel.DenoteExternalCalls.AdversaryModel) + ($(⟨transitionIdent.raw⟩) : Verity.ContractState → Verity.ContractState) : Prop := + $registryBody) let bodyCmd : Cmd ← `(command| def $modelBodyName : List Compiler.CompilationModel.Stmt := [ $[$stmtTerms],* ]) let modelNameTerm := if fn.isInternal then @@ -5592,7 +5627,23 @@ def mkFunctionCommandsPublic body := $modelBodyName isInternal := $internalTerm }) - pure #[fnCmd, bodyCmd, modelCmd] + pure #[fnCmd, entrypointCmd, bodyCmd, modelCmd] + +/-- Emit the contract-wide union of all externally callable entrypoint +predicates. Each per-function predicate keeps arguments existential and uses +the registry's explicit adversary when the function opens a reentrancy window. -/ +def mkEntrypointRegistryCommandPublic (functions : Array FunctionDecl) : CommandElabM Cmd := do + let advIdent ← Lean.Elab.Term.mkFreshIdent (mkIdent `_registryAdv).raw + let transitionIdent ← Lean.Elab.Term.mkFreshIdent (mkIdent `_transition).raw + let mut body : Term ← `(False) + for fn in functions.reverse do + unless fn.isInternal do + let predicateName ← mkSuffixedIdent fn.ident "_entrypoint" + body ← `($predicateName $(⟨advIdent.raw⟩) $(⟨transitionIdent.raw⟩) ∨ $body) + let id := mkIdent (Name.mkSimple "entrypointRegistry") + `(command| + def $id : Compiler.CompilationModel.DenoteExternalCalls.EntrypointRegistry := + fun $(⟨advIdent.raw⟩) $(⟨transitionIdent.raw⟩) => $body) def mkSpecCommandPublic (contractName : String) From b688deed98287bb38164e3d39b1a656cc63a1352 Mon Sep 17 00:00:00 2001 From: Thomas Marchand Date: Sun, 6 Sep 2026 23:56:32 +0000 Subject: [PATCH 02/50] fix(macro): load registry semantics at elaboration boundary --- Verity/Macro/Elaborate.lean | 2 ++ Verity/Macro/Translate.lean | 2 -- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/Verity/Macro/Elaborate.lean b/Verity/Macro/Elaborate.lean index 723755bd4..ac684bb20 100644 --- a/Verity/Macro/Elaborate.lean +++ b/Verity/Macro/Elaborate.lean @@ -5,6 +5,8 @@ import Verity.Macro.Translate import Verity.Macro.Bridge import Verity.Core.Intrinsics import Verity.Core.Uint256 +import Verity.Core.Model.CallbackBridge +import Verity.Core.Model.NonReentrantGuard namespace Verity.Macro diff --git a/Verity/Macro/Translate.lean b/Verity/Macro/Translate.lean index 031ff08a5..a4f6a26dd 100644 --- a/Verity/Macro/Translate.lean +++ b/Verity/Macro/Translate.lean @@ -16,8 +16,6 @@ import Verity.Macro.Internal import Verity.Macro.Storage import Verity.Macro.Types import Verity.Macro.Syntax -import Verity.Core.Model.CallbackBridge -import Verity.Core.Model.NonReentrantGuard namespace Verity.Macro From 8a1d59008774114c47fd956a18df27c7b2d11df5 Mon Sep 17 00:00:00 2001 From: Thomas Marchand Date: Mon, 7 Sep 2026 00:01:53 +0000 Subject: [PATCH 03/50] fix(macro): construct registry binders hygienically --- Verity/Macro/Translate.lean | 23 ++++++++++++++--------- 1 file changed, 14 insertions(+), 9 deletions(-) diff --git a/Verity/Macro/Translate.lean b/Verity/Macro/Translate.lean index a4f6a26dd..c8808d802 100644 --- a/Verity/Macro/Translate.lean +++ b/Verity/Macro/Translate.lean @@ -5578,26 +5578,29 @@ def mkFunctionCommandsPublic (mkIdentFrom fn.ident `_registryAdv).raw let transitionIdent ← Lean.Elab.Term.mkFreshIdent (mkIdentFrom fn.ident `_transition).raw + let registryAdv : Ident := ⟨registryAdvIdent.raw⟩ + let transition : Ident := ⟨transitionIdent.raw⟩ let mut applied : Term := fn.ident if opensReentrancyWindow then - applied ← `($applied (ExecutableCallContext.ofAdversary $(⟨registryAdvIdent.raw⟩))) + applied ← `($applied (ExecutableCallContext.ofAdversary $registryAdv:ident)) let mut registryParams : Array (Ident × Term) := #[] for param in fn.params do let paramTy ← contractValueTypeTerm param.ty let paramIdent ← Lean.Elab.Term.mkFreshIdent (mkIdentFrom param.ident `_registryArg).raw - registryParams := registryParams.push (⟨paramIdent.raw⟩, paramTy) - applied ← `($applied $(⟨paramIdent.raw⟩)) + let registryParam : Ident := ⟨paramIdent.raw⟩ + registryParams := registryParams.push (registryParam, paramTy) + applied ← `($applied $registryParam:ident) let mut registryBody : Term ← - `(($(⟨transitionIdent.raw⟩) : Verity.ContractState → Verity.ContractState) = + `(($transition:ident : Verity.ContractState → Verity.ContractState) = ($applied).runState) for (paramIdent, paramTy) in registryParams.reverse do - registryBody ← `(∃ $paramIdent : $paramTy, $registryBody) + registryBody ← `(∃ $paramIdent:ident : $paramTy, $registryBody) let entrypointCmd : Cmd ← `(command| def $entrypointPredicateName - ($(⟨registryAdvIdent.raw⟩) : + ($registryAdv:ident : Compiler.CompilationModel.DenoteExternalCalls.AdversaryModel) - ($(⟨transitionIdent.raw⟩) : Verity.ContractState → Verity.ContractState) : Prop := + ($transition:ident : Verity.ContractState → Verity.ContractState) : Prop := $registryBody) let bodyCmd : Cmd ← `(command| def $modelBodyName : List Compiler.CompilationModel.Stmt := [ $[$stmtTerms],* ]) let modelNameTerm := @@ -5633,15 +5636,17 @@ the registry's explicit adversary when the function opens a reentrancy window. - def mkEntrypointRegistryCommandPublic (functions : Array FunctionDecl) : CommandElabM Cmd := do let advIdent ← Lean.Elab.Term.mkFreshIdent (mkIdent `_registryAdv).raw let transitionIdent ← Lean.Elab.Term.mkFreshIdent (mkIdent `_transition).raw + let registryAdv : Ident := ⟨advIdent.raw⟩ + let transition : Ident := ⟨transitionIdent.raw⟩ let mut body : Term ← `(False) for fn in functions.reverse do unless fn.isInternal do let predicateName ← mkSuffixedIdent fn.ident "_entrypoint" - body ← `($predicateName $(⟨advIdent.raw⟩) $(⟨transitionIdent.raw⟩) ∨ $body) + body ← `($predicateName $registryAdv:ident $transition:ident ∨ $body) let id := mkIdent (Name.mkSimple "entrypointRegistry") `(command| def $id : Compiler.CompilationModel.DenoteExternalCalls.EntrypointRegistry := - fun $(⟨advIdent.raw⟩) $(⟨transitionIdent.raw⟩) => $body) + fun $registryAdv:ident $transition:ident => $body) def mkSpecCommandPublic (contractName : String) From 54c7273996b935a2811c67cea04973b9a8cedbfb Mon Sep 17 00:00:00 2001 From: Thomas Marchand Date: Mon, 7 Sep 2026 00:39:45 +0000 Subject: [PATCH 04/50] proof(reentrancy): consume generated callback registry --- .../ReentrancyRelyGuarantee/Contract.lean | 18 +++++++ .../GeneratedRegistry.lean | 48 ++++++++++++++++++ Verity/Core/Model/CallbackBridge.lean | 49 +++++++++++++++++++ Verity/Macro.lean | 2 + Verity/Macro/Elaborate.lean | 2 - 5 files changed, 117 insertions(+), 2 deletions(-) create mode 100644 Contracts/ReentrancyRelyGuarantee/GeneratedRegistry.lean diff --git a/Contracts/ReentrancyRelyGuarantee/Contract.lean b/Contracts/ReentrancyRelyGuarantee/Contract.lean index 9871914cc..f30dc7cf8 100644 --- a/Contracts/ReentrancyRelyGuarantee/Contract.lean +++ b/Contracts/ReentrancyRelyGuarantee/Contract.lean @@ -22,6 +22,7 @@ import Verity.Core import Verity.Core.Semantics import Verity.Core.Reentrancy import Verity.Core.Model.CallbackBridge +import Contracts.ReentrancyRelyGuarantee.GeneratedRegistry namespace Contracts.ReentrancyRelyGuarantee @@ -217,4 +218,21 @@ theorem callback_bounded_transaction_preserves_I I (denoteTransaction prog adversary state).state.world := hbound.transaction_preserves spec prog state hInv +/-! ## Generated-registry consumer boundary -/ + +/- `ReentrancyRelyGuarantee` consumes the macro-emitted registry directly at +the callback boundary. This deliberately small invariant isolates the PR4 +connection; contract-specific preservation obligations remain with authors. -/ +open Compiler.CompilationModel.DenoteExternalCalls in +theorem generated_registry_callback_preserves + {adversary : AdversaryModel} + (hbound : CallbackBounded GeneratedRegistry.entrypointRegistry adversary) + (hregistry : RegistryPreserves (fun _ => True) + GeneratedRegistry.entrypointRegistry adversary) + (site : CallSite) (state : CallState) : + (fun _ : ContractState => True) + (denoteCall adversary site state).state.world := + hbound.denoteCall_preserves_registry (fun _ => True) + GeneratedRegistry.entrypointRegistry hregistry site state trivial + end Contracts.ReentrancyRelyGuarantee diff --git a/Contracts/ReentrancyRelyGuarantee/GeneratedRegistry.lean b/Contracts/ReentrancyRelyGuarantee/GeneratedRegistry.lean new file mode 100644 index 000000000..de24734ff --- /dev/null +++ b/Contracts/ReentrancyRelyGuarantee/GeneratedRegistry.lean @@ -0,0 +1,48 @@ +import Contracts.Common +import Verity.Core.Model.CallbackBridge +import Verity.Core.Model.NonReentrantGuard + +namespace Contracts.ReentrancyRelyGuarantee + +open Contracts +open Verity hiding pure bind + +/-! Focused generated consumer for the registry/guard boundary. It contains +an actual mutable external-call window, so the executable entrypoint must take +an explicit adversary and the nonreentrant annotation must guard that same +generated function. -/ +verity_contract GeneratedRegistry where + storage + lock : Uint256 := slot 0 + linked_externals + external ping(Uint256) -> (Uint256) + + function nonreentrant(lock) guardedPing (value : Uint256) : Unit := do + let _response := externalCall "ping" [value] + return () + + function noop (value : Uint256) : Uint256 := do + return value + +namespace GeneratedRegistry + +open Compiler.CompilationModel.DenoteExternalCalls + +/-- The generated registry uses its explicit adversary at the external-call +entrypoint; there is no `.stub` compatibility path in this theorem surface. -/ +theorem guardedPing_registered (adv : AdversaryModel) (value : Uint256) : + entrypointRegistry adv + (guardedPing (ExecutableCallContext.ofAdversary adv) value).runState := by + left + exact ⟨value, rfl⟩ + +/-- The executable generated entrypoint is definitionally protected by the +canonical source guard at the same slot used by the compiled dispatch guard. -/ +theorem guardedPing_reentry_blocked (adv : AdversaryModel) (value : Uint256) + (state : ContractState) (hlock : state.transientStorage 0 ≠ 0) : + (guardedPing (ExecutableCallContext.ofAdversary adv) value).runState state = state := by + apply Verity.Core.NonReentrantGuard.guarded_reentry_blocked + exact hlock + +end GeneratedRegistry +end Contracts.ReentrancyRelyGuarantee diff --git a/Verity/Core/Model/CallbackBridge.lean b/Verity/Core/Model/CallbackBridge.lean index 40f2ba0c0..f2dae8866 100644 --- a/Verity/Core/Model/CallbackBridge.lean +++ b/Verity/Core/Model/CallbackBridge.lean @@ -49,6 +49,55 @@ def CallbackBounded (∀ f ∈ sched, entrypoints adversary f) ∧ adversary.stateTransition site world = runSeq sched world +/-- The sole proof obligation at the generated-registry boundary: every +transition admitted by the registry for this adversary preserves the caller's +invariant. -/ +def RegistryPreserves (Inv : Verity.ContractState → Prop) + (entrypoints : EntrypointRegistry) (adversary : AdversaryModel) : Prop := + ∀ f, entrypoints adversary f → Preserves Inv f + +/-- A call through the restricted generated-registry boundary preserves any +invariant discharged for every registered, fully-applied entrypoint. -/ +theorem CallbackBounded.denoteCall_preserves_registry + (Inv : Verity.ContractState → Prop) (entrypoints : EntrypointRegistry) + {adversary : AdversaryModel} + (hbound : CallbackBounded entrypoints adversary) + (hregistry : RegistryPreserves Inv entrypoints adversary) + (site : CallSite) (state : CallState) (hInv : Inv state.world) : + Inv (denoteCall adversary site state).state.world := by + cases hkind : site.kind with + | staticcall => + rw [denoteCall_staticcall_world adversary site state hkind] + exact hInv + | call => + cases hres : adversary.result site state.world with + | success data => + rw [denoteCall_call_success_world adversary site state data hkind hres] + obtain ⟨sched, hmem, htrans⟩ := hbound site state.world (by simp [hkind]) + rw [htrans] + exact Verity.Core.Invariant.runSeq_preserves sched + (fun f hf => hregistry f (hmem f hf)) state.world hInv + | failure data => + rw [denoteCall_failure_world adversary site state data (Or.inl hkind) hres] + exact hInv + | revert data => + rw [denoteCall_revert_world adversary site state data (Or.inl hkind) hres] + exact hInv + | delegatecall => + cases hres : adversary.result site state.world with + | success data => + rw [denoteCall_delegatecall_success_world adversary site state data hkind hres] + obtain ⟨sched, hmem, htrans⟩ := hbound site state.world (by simp [hkind]) + rw [htrans] + exact Verity.Core.Invariant.runSeq_preserves sched + (fun f hf => hregistry f (hmem f hf)) state.world hInv + | failure data => + rw [denoteCall_failure_world adversary site state data (Or.inr hkind) hres] + exact hInv + | revert data => + rw [denoteCall_revert_world adversary site state data (Or.inr hkind) hres] + exact hInv + /-- One external call under a callback-bounded adversary preserves the spec invariant: rollback outcomes keep the pre-call world, and committed outcomes are reentry schedules, covered by the per-entrypoint obligations. -/ diff --git a/Verity/Macro.lean b/Verity/Macro.lean index 876e5efd2..ba5feda44 100644 --- a/Verity/Macro.lean +++ b/Verity/Macro.lean @@ -2,6 +2,8 @@ import Verity.Macro.Syntax import Verity.Macro.Translate import Verity.Macro.Bridge import Verity.Macro.Elaborate +import Verity.Core.Model.CallbackBridge +import Verity.Core.Model.NonReentrantGuard import Verity.Macro.SpecGen import Verity.Macro.KeccakLit import Verity.Macro.KeccakString diff --git a/Verity/Macro/Elaborate.lean b/Verity/Macro/Elaborate.lean index ac684bb20..723755bd4 100644 --- a/Verity/Macro/Elaborate.lean +++ b/Verity/Macro/Elaborate.lean @@ -5,8 +5,6 @@ import Verity.Macro.Translate import Verity.Macro.Bridge import Verity.Core.Intrinsics import Verity.Core.Uint256 -import Verity.Core.Model.CallbackBridge -import Verity.Core.Model.NonReentrantGuard namespace Verity.Macro From 4af7eaf829f585491aea8c3b8dacb03319667744 Mon Sep 17 00:00:00 2001 From: Thomas Marchand Date: Mon, 7 Sep 2026 00:47:39 +0000 Subject: [PATCH 05/50] chore(audit): sync registry consumer artifacts --- Contracts/ReentrancyRelyGuarantee/Contract.lean | 2 +- .../Proofs/Model/GeneratedEntrypointRegistry.lean | 0 artifacts/verification_status.json | 12 ++++++------ docs/VERIFICATION_STATUS.md | 14 +++++++------- test/property_exclusions.json | 1 + test/property_manifest.json | 1 + 6 files changed, 16 insertions(+), 14 deletions(-) rename Contracts/ReentrancyRelyGuarantee/GeneratedRegistry.lean => Verity/Proofs/Model/GeneratedEntrypointRegistry.lean (100%) diff --git a/Contracts/ReentrancyRelyGuarantee/Contract.lean b/Contracts/ReentrancyRelyGuarantee/Contract.lean index f30dc7cf8..1a26cdd81 100644 --- a/Contracts/ReentrancyRelyGuarantee/Contract.lean +++ b/Contracts/ReentrancyRelyGuarantee/Contract.lean @@ -22,7 +22,7 @@ import Verity.Core import Verity.Core.Semantics import Verity.Core.Reentrancy import Verity.Core.Model.CallbackBridge -import Contracts.ReentrancyRelyGuarantee.GeneratedRegistry +import Verity.Proofs.Model.GeneratedEntrypointRegistry namespace Contracts.ReentrancyRelyGuarantee diff --git a/Contracts/ReentrancyRelyGuarantee/GeneratedRegistry.lean b/Verity/Proofs/Model/GeneratedEntrypointRegistry.lean similarity index 100% rename from Contracts/ReentrancyRelyGuarantee/GeneratedRegistry.lean rename to Verity/Proofs/Model/GeneratedEntrypointRegistry.lean diff --git a/artifacts/verification_status.json b/artifacts/verification_status.json index 3ed6aeae4..401522c40 100644 --- a/artifacts/verification_status.json +++ b/artifacts/verification_status.json @@ -16,10 +16,10 @@ }, "theorems": { "categories": 15, - "coverage_percent": 78, + "coverage_percent": 77, "covered": 255, - "excluded": 74, - "non_stdlib_total": 329, + "excluded": 75, + "non_stdlib_total": 330, "per_contract": { "Counter": 31, "ERC20": 22, @@ -31,15 +31,15 @@ "OwnedCounter": 63, "OwnedCounterComposed": 6, "ReentrancyExample": 5, - "ReentrancyRelyGuarantee": 10, + "ReentrancyRelyGuarantee": 11, "SafeCounter": 25, "SimpleStorage": 20, "SimpleToken": 61, "Vault": 9 }, - "proven": 329, + "proven": 330, "stdlib": 0, - "total": 329 + "total": 330 }, "toolchain": { "lean": "leanprover/lean4:v4.31.0", diff --git a/docs/VERIFICATION_STATUS.md b/docs/VERIFICATION_STATUS.md index 08f468438..87a316638 100644 --- a/docs/VERIFICATION_STATUS.md +++ b/docs/VERIFICATION_STATUS.md @@ -40,11 +40,11 @@ EVM Bytecode | ERC721 | 11 | Baseline | `Contracts/ERC721/Proofs/` | | Vault | 9 | Baseline | `Contracts/Vault/Proofs/` | | ReentrancyExample | 5 | Complete | `Contracts/ReentrancyExample/Contract.lean` | -| ReentrancyRelyGuarantee | 10 | Semantic | `Contracts/ReentrancyRelyGuarantee/Contract.lean` | +| ReentrancyRelyGuarantee | 11 | Semantic | `Contracts/ReentrancyRelyGuarantee/Contract.lean` | | CryptoHash | 0 | No specs | `Contracts/CryptoHash/Contract.lean` | -| **Total** | **329** | **✅ 100%** | — | +| **Total** | **330** | **✅ 100%** | — | -> **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). +> **Note**: Stdlib (0 internal proof-automation properties) is excluded from the contract-spec theorem table above but included in overall coverage statistics (330 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. @@ -205,7 +205,7 @@ Also note that the macro-generated `*_semantic_preservation` theorems are not co | ERC721 | 100% (11/11) | 0 | | SafeCounter | 100% (25/25) | 0 | | ReentrancyExample | 100% (5/5) | 0 | -| ReentrancyRelyGuarantee | 0% (0/10) | 10 proof-only | +| ReentrancyRelyGuarantee | 0% (0/11) | 11 proof-only | | Ledger | 100% (33/33) | 0 | | LocalObligationMacroSmoke | 100% (4/4) | 0 | | SimpleStorage | 95% (19/20) | 1 proof-only | @@ -217,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**: 78% coverage (255/329), 74 remaining exclusions all proof-only +**Status**: 77% coverage (255/330), 75 remaining exclusions all proof-only -- **Total Properties**: 329 +- **Total Properties**: 330 - **Covered**: 255 -- **Excluded**: 74 (all proof-only) +- **Excluded**: 75 (all proof-only) **Proof-Only Properties (59 exclusions)**: Internal proof machinery that cannot be tested in Foundry. diff --git a/test/property_exclusions.json b/test/property_exclusions.json index b02ea2d27..788bff1db 100644 --- a/test/property_exclusions.json +++ b/test/property_exclusions.json @@ -87,6 +87,7 @@ "buggy_admits_permanent_bad_debt", "callback_bounded_program_preserves_I", "callback_bounded_transaction_preserves_I", + "generated_registry_callback_preserves", "liquidate_preserves_I", "liquidate_respects_lock", "locked_blocks_concrete_liquidation", diff --git a/test/property_manifest.json b/test/property_manifest.json index b43566798..45b508baf 100644 --- a/test/property_manifest.json +++ b/test/property_manifest.json @@ -229,6 +229,7 @@ "buggy_admits_permanent_bad_debt", "callback_bounded_program_preserves_I", "callback_bounded_transaction_preserves_I", + "generated_registry_callback_preserves", "liquidate_preserves_I", "liquidate_respects_lock", "locked_blocks_concrete_liquidation", From c98bcbc8b6568a67ffbd828a330128d48e6937b2 Mon Sep 17 00:00:00 2001 From: Thomas Marchand Date: Mon, 7 Sep 2026 00:48:20 +0000 Subject: [PATCH 06/50] chore(audit): register generated registry proofs --- PrintAxioms.lean | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/PrintAxioms.lean b/PrintAxioms.lean index bbf05cfad..0fb2c66d7 100644 --- a/PrintAxioms.lean +++ b/PrintAxioms.lean @@ -38,6 +38,7 @@ import Contracts.Vault.Proofs.Native import Verity.Proofs.CheckedExternalCallConsumer import Verity.Proofs.LoopSimulationResultAware import Verity.Proofs.Model.CommonExternalCallEquivalence +import Verity.Proofs.Model.GeneratedEntrypointRegistry import Verity.Proofs.Stdlib.Automation import Verity.Proofs.Stdlib.ListSum import Verity.Proofs.Stdlib.MappingAutomation @@ -718,6 +719,10 @@ end Verity.AxiomAudit Contracts.legacyStringSafeTransfer_eq_stub Contracts.legacyStringSafeTransferFrom_eq_stub + -- Verity/Proofs/Model/GeneratedEntrypointRegistry.lean + Contracts.ReentrancyRelyGuarantee.GeneratedRegistry.guardedPing_registered + Contracts.ReentrancyRelyGuarantee.GeneratedRegistry.guardedPing_reentry_blocked + -- Verity/Proofs/Stdlib/Automation.lean Verity.Proofs.Stdlib.Automation.isSuccess_success Verity.Proofs.Stdlib.Automation.isSuccess_revert @@ -7515,4 +7520,4 @@ end Verity.AxiomAudit Compiler.Proofs.YulGeneration.YulTransaction.ofIR_args ] --- Total: 6953 theorems/lemmas (4963 public, 1990 private, 0 sorry'd) +-- Total: 6955 theorems/lemmas (4965 public, 1990 private, 0 sorry'd) From ccf79f911a10433b2503fd1aa7b358da5c9472bc Mon Sep 17 00:00:00 2001 From: Thomas Marchand Date: Mon, 7 Sep 2026 00:50:56 +0000 Subject: [PATCH 07/50] chore(docs): sync proof counts --- docs-site/public/llms.txt | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/docs-site/public/llms.txt b/docs-site/public/llms.txt index b4b89b664..6cb26e71a 100644 --- a/docs-site/public/llms.txt +++ b/docs-site/public/llms.txt @@ -31,9 +31,9 @@ Every transition inside the proof envelope is either fully verified or recorded - **Language**: Lean 4.31.0 -- **Core Size**: 1991 lines +- **Core Size**: 2040 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 +- **Theorems**: 330 across 15 categories, 330 fully proven - **Axioms**: 1 documented Lean axioms (see AXIOMS.md) - **Tests**: 528 Foundry tests, 239 property tests - **Build**: `lake build` verifies all proofs From ed2b7c089e3b2dca7d425dac5af82c587b5b5cd4 Mon Sep 17 00:00:00 2001 From: Thomas Marchand Date: Mon, 7 Sep 2026 09:26:33 +0000 Subject: [PATCH 08/50] fix(reentrancy): preserve no-call contract surface --- .../ReentrancyRelyGuarantee/Contract.lean | 22 ++----------------- Verity/Core/Model/CallbackBridge.lean | 4 ++++ .../Model/GeneratedEntrypointRegistry.lean | 15 +++++++++++++ 3 files changed, 21 insertions(+), 20 deletions(-) diff --git a/Contracts/ReentrancyRelyGuarantee/Contract.lean b/Contracts/ReentrancyRelyGuarantee/Contract.lean index 1a26cdd81..1ca927351 100644 --- a/Contracts/ReentrancyRelyGuarantee/Contract.lean +++ b/Contracts/ReentrancyRelyGuarantee/Contract.lean @@ -22,7 +22,6 @@ import Verity.Core import Verity.Core.Semantics import Verity.Core.Reentrancy import Verity.Core.Model.CallbackBridge -import Verity.Proofs.Model.GeneratedEntrypointRegistry namespace Contracts.ReentrancyRelyGuarantee @@ -204,7 +203,7 @@ any `CallProgram`, and through the transaction commit/revert boundary. -/ open Compiler.CompilationModel.DenoteExternalCalls in theorem callback_bounded_program_preserves_I {adversary : AdversaryModel} - (hbound : CallbackBounded (EntrypointRegistry.ofList spec.entrypoints) adversary) + (hbound : CallbackBounded spec.entrypoints adversary) (prog : CallProgram α) (state : CallState) (hInv : I state.world) : I (denote prog adversary state).2.world := hbound.denote_preserves spec prog state hInv @@ -212,27 +211,10 @@ theorem callback_bounded_program_preserves_I open Compiler.CompilationModel.DenoteExternalCalls in theorem callback_bounded_transaction_preserves_I {adversary : AdversaryModel} {α : Type} - (hbound : CallbackBounded (EntrypointRegistry.ofList spec.entrypoints) adversary) + (hbound : CallbackBounded spec.entrypoints adversary) (prog : CallProgram (TransactionResult α)) (state : CallState) (hInv : I state.world) : I (denoteTransaction prog adversary state).state.world := hbound.transaction_preserves spec prog state hInv -/-! ## Generated-registry consumer boundary -/ - -/- `ReentrancyRelyGuarantee` consumes the macro-emitted registry directly at -the callback boundary. This deliberately small invariant isolates the PR4 -connection; contract-specific preservation obligations remain with authors. -/ -open Compiler.CompilationModel.DenoteExternalCalls in -theorem generated_registry_callback_preserves - {adversary : AdversaryModel} - (hbound : CallbackBounded GeneratedRegistry.entrypointRegistry adversary) - (hregistry : RegistryPreserves (fun _ => True) - GeneratedRegistry.entrypointRegistry adversary) - (site : CallSite) (state : CallState) : - (fun _ : ContractState => True) - (denoteCall adversary site state).state.world := - hbound.denoteCall_preserves_registry (fun _ => True) - GeneratedRegistry.entrypointRegistry hregistry site state trivial - end Contracts.ReentrancyRelyGuarantee diff --git a/Verity/Core/Model/CallbackBridge.lean b/Verity/Core/Model/CallbackBridge.lean index f2dae8866..782c64e64 100644 --- a/Verity/Core/Model/CallbackBridge.lean +++ b/Verity/Core/Model/CallbackBridge.lean @@ -36,6 +36,10 @@ def ofList (entrypoints : List (Verity.ContractState → Verity.ContractState)) EntrypointRegistry := fun _ entrypoint => entrypoint ∈ entrypoints +instance : Coe (List (Verity.ContractState → Verity.ContractState)) + EntrypointRegistry where + coe := ofList + end EntrypointRegistry /-- Each mutable transition is some finite reentry schedule drawn from the diff --git a/Verity/Proofs/Model/GeneratedEntrypointRegistry.lean b/Verity/Proofs/Model/GeneratedEntrypointRegistry.lean index de24734ff..709ee0020 100644 --- a/Verity/Proofs/Model/GeneratedEntrypointRegistry.lean +++ b/Verity/Proofs/Model/GeneratedEntrypointRegistry.lean @@ -45,4 +45,19 @@ theorem guardedPing_reentry_blocked (adv : AdversaryModel) (value : Uint256) exact hlock end GeneratedRegistry + +/-- `ReentrancyRelyGuarantee` consumes the emitted registry at the restricted +callback boundary. Contract-specific preservation obligations remain with +authors; this PR establishes only the generated registry/guard connection. -/ +theorem generated_registry_callback_preserves + {adversary : AdversaryModel} + (hbound : CallbackBounded GeneratedRegistry.entrypointRegistry adversary) + (hregistry : RegistryPreserves (fun _ => True) + GeneratedRegistry.entrypointRegistry adversary) + (site : CallSite) (state : CallState) : + (fun _ : ContractState => True) + (denoteCall adversary site state).state.world := + hbound.denoteCall_preserves_registry (fun _ => True) + GeneratedRegistry.entrypointRegistry hregistry site state trivial + end Contracts.ReentrancyRelyGuarantee From 0411dfa04d83249df05b79527d10c04ab1f57ee3 Mon Sep 17 00:00:00 2001 From: "github-actions[bot]" Date: Mon, 7 Sep 2026 10:30:27 +0100 Subject: [PATCH 09/50] chore: auto-refresh derived artifacts --- PrintAxioms.lean | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/PrintAxioms.lean b/PrintAxioms.lean index 0fb2c66d7..298c51198 100644 --- a/PrintAxioms.lean +++ b/PrintAxioms.lean @@ -722,6 +722,7 @@ end Verity.AxiomAudit -- Verity/Proofs/Model/GeneratedEntrypointRegistry.lean Contracts.ReentrancyRelyGuarantee.GeneratedRegistry.guardedPing_registered Contracts.ReentrancyRelyGuarantee.GeneratedRegistry.guardedPing_reentry_blocked + Contracts.ReentrancyRelyGuarantee.generated_registry_callback_preserves -- Verity/Proofs/Stdlib/Automation.lean Verity.Proofs.Stdlib.Automation.isSuccess_success @@ -7520,4 +7521,4 @@ end Verity.AxiomAudit Compiler.Proofs.YulGeneration.YulTransaction.ofIR_args ] --- Total: 6955 theorems/lemmas (4965 public, 1990 private, 0 sorry'd) +-- Total: 6956 theorems/lemmas (4966 public, 1990 private, 0 sorry'd) From c6a4a973b7d4e41e01dc51aa73ce87a99609ae20 Mon Sep 17 00:00:00 2001 From: Thomas Marchand Date: Mon, 7 Sep 2026 09:37:57 +0000 Subject: [PATCH 10/50] chore(audit): sync registry proof manifest --- test/property_manifest.json | 1 - 1 file changed, 1 deletion(-) diff --git a/test/property_manifest.json b/test/property_manifest.json index 45b508baf..b43566798 100644 --- a/test/property_manifest.json +++ b/test/property_manifest.json @@ -229,7 +229,6 @@ "buggy_admits_permanent_bad_debt", "callback_bounded_program_preserves_I", "callback_bounded_transaction_preserves_I", - "generated_registry_callback_preserves", "liquidate_preserves_I", "liquidate_respects_lock", "locked_blocks_concrete_liquidation", From d51100ab7acac7040483203efd97f335942c083a Mon Sep 17 00:00:00 2001 From: Thomas Marchand Date: Mon, 7 Sep 2026 09:38:27 +0000 Subject: [PATCH 11/50] chore(audit): sync registry proof exclusions --- test/property_exclusions.json | 1 - 1 file changed, 1 deletion(-) diff --git a/test/property_exclusions.json b/test/property_exclusions.json index 788bff1db..b02ea2d27 100644 --- a/test/property_exclusions.json +++ b/test/property_exclusions.json @@ -87,7 +87,6 @@ "buggy_admits_permanent_bad_debt", "callback_bounded_program_preserves_I", "callback_bounded_transaction_preserves_I", - "generated_registry_callback_preserves", "liquidate_preserves_I", "liquidate_respects_lock", "locked_blocks_concrete_liquidation", From 2e53a639fa33366ab4684bc510418b26f31f6fba Mon Sep 17 00:00:00 2001 From: "github-actions[bot]" Date: Mon, 7 Sep 2026 11:42:37 +0200 Subject: [PATCH 12/50] chore: auto-refresh derived artifacts --- artifacts/verification_status.json | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/artifacts/verification_status.json b/artifacts/verification_status.json index 401522c40..3ed6aeae4 100644 --- a/artifacts/verification_status.json +++ b/artifacts/verification_status.json @@ -16,10 +16,10 @@ }, "theorems": { "categories": 15, - "coverage_percent": 77, + "coverage_percent": 78, "covered": 255, - "excluded": 75, - "non_stdlib_total": 330, + "excluded": 74, + "non_stdlib_total": 329, "per_contract": { "Counter": 31, "ERC20": 22, @@ -31,15 +31,15 @@ "OwnedCounter": 63, "OwnedCounterComposed": 6, "ReentrancyExample": 5, - "ReentrancyRelyGuarantee": 11, + "ReentrancyRelyGuarantee": 10, "SafeCounter": 25, "SimpleStorage": 20, "SimpleToken": 61, "Vault": 9 }, - "proven": 330, + "proven": 329, "stdlib": 0, - "total": 330 + "total": 329 }, "toolchain": { "lean": "leanprover/lean4:v4.31.0", From d946bdf1451e2520adb2e91aa920ccdca5617e4d Mon Sep 17 00:00:00 2001 From: Thomas Marchand Date: Mon, 7 Sep 2026 13:00:47 +0200 Subject: [PATCH 13/50] fix(reentrancy): align guarded registry semantics --- Compiler/CompilationModel/Dispatch.lean | 4 +-- .../IRGeneration/NonReentrantGuardIR.lean | 26 +++++++-------- Verity/Macro/Translate.lean | 32 +++++++++++++++++-- docs-site/public/llms.txt | 2 +- docs/VERIFICATION_STATUS.md | 14 ++++---- 5 files changed, 51 insertions(+), 27 deletions(-) diff --git a/Compiler/CompilationModel/Dispatch.lean b/Compiler/CompilationModel/Dispatch.lean index 8bd89a6bd..370bf0958 100644 --- a/Compiler/CompilationModel/Dispatch.lean +++ b/Compiler/CompilationModel/Dispatch.lean @@ -205,7 +205,7 @@ def compileFunctionSpec (fields : List Field) (events : List EventDef) (errors : The emitted Yul is: ```yul - if eq(tload(), 1) { revert(0, 0) } + if tload() { revert(0, 0) } tstore(, 1) ``` @@ -224,7 +224,7 @@ def nonReentrantGuardPrologue (fields : List Field) (lockField : String) : let lockSlot := YulExpr.lit slot let revertOnReentry := YulStmt.if_ - (YulExpr.call "eq" [YulExpr.call "tload" [lockSlot], YulExpr.lit 1]) + (YulExpr.call "tload" [lockSlot]) [YulStmt.exprStmt (YulExpr.call "revert" [YulExpr.lit 0, YulExpr.lit 0])] let acquire := YulStmt.exprStmt (YulExpr.call "tstore" [lockSlot, YulExpr.lit 1]) diff --git a/Compiler/Proofs/IRGeneration/NonReentrantGuardIR.lean b/Compiler/Proofs/IRGeneration/NonReentrantGuardIR.lean index 3a5729f4f..2d95c80b1 100644 --- a/Compiler/Proofs/IRGeneration/NonReentrantGuardIR.lean +++ b/Compiler/Proofs/IRGeneration/NonReentrantGuardIR.lean @@ -9,12 +9,12 @@ First machine-checked brick of the `guarded` ↔ emitted-Yul correspondence `Compiler.CompilationModel.nonReentrantGuardPrologue` are evaluated under the IR interpreter used by the IR-generation proofs. -- lock slot reads `1` → the frame reverts with the state untouched; +- lock slot reads nonzero → the frame reverts with the state untouched; - lock slot reads `0` → execution falls through with the lock set to `1` and nothing else changed; - the release statement spliced by `applyLockReleaseOnExits` resets the slot; -- on the reachable (binary) lock values, the Yul decision `eq(tload(slot), 1)` - agrees with the source-model decision `lock ≠ 0` of +- the Yul decision on `tload(slot)` agrees with the source-model decision + `lock ≠ 0` of `Verity.Core.Model.NonReentrantGuard.guarded`. Still open: pushing these statement-level facts through @@ -29,7 +29,7 @@ open Compiler.CompilationModel /-- The exact prologue shape emitted for a resolved lock slot. -/ def guardPrologueStmts (slot : Nat) : List YulStmt := - [ .if_ (.call "eq" [.call "tload" [.lit slot], .lit 1]) + [ .if_ (.call "tload" [.lit slot]) [.exprStmt (.call "revert" [.lit 0, .lit 0])], .exprStmt (.call "tstore" [.lit slot, .lit 1]) ] @@ -45,22 +45,20 @@ theorem nonReentrantGuardPrologue_eq (fields : List Field) (lockField : String) nonReentrantGuardPrologue fields lockField = .ok (guardPrologueStmts slot) := by simp [nonReentrantGuardPrologue, h, guardPrologueStmts, pure, Except.pure] -/-- Lock held (`tload = 1`) → the prologue reverts and the state is untouched. -/ +/-- Lock held (`tload ≠ 0`) → the prologue reverts and the state is untouched. -/ theorem execIRStmts_guardPrologue_locked (fuel : Nat) (state : IRState) (slot : Nat) (hslot : slot < Compiler.Constants.evmModulus) - (hlock : state.transientStorage slot = 1) : + (hlock : state.transientStorage slot ≠ 0) : execIRStmts (fuel + 3) state (guardPrologueStmts slot) = .revert state := by have hmod : slot % Compiler.Constants.evmModulus = slot := Nat.mod_eq_of_lt hslot - have hone : (1 : Nat) < Compiler.Constants.evmModulus := by - simp [Compiler.Constants.evmModulus] cases fuel with | zero => simp [guardPrologueStmts, execIRStmts, execIRStmt, evalIRExpr, evalIRCall, - evalIRExprs, hmod, hlock, Nat.mod_eq_of_lt hone, + evalIRExprs, hmod, hlock, YulGeneration.Backends.evalBuiltinCallWithEvmYulLeanContext] | succ n => simp [guardPrologueStmts, execIRStmts, execIRStmt, evalIRExpr, evalIRCall, - evalIRExprs, hmod, hlock, Nat.mod_eq_of_lt hone, + evalIRExprs, hmod, hlock, YulGeneration.Backends.evalBuiltinCallWithEvmYulLeanContext] /-- Lock free (`tload = 0`) → the prologue acquires the lock and changes @@ -87,11 +85,9 @@ theorem execIRStmt_lockRelease (fuel : Nat) (state : IRState) (slot : Nat) have hmod : slot % Compiler.Constants.evmModulus = slot := Nat.mod_eq_of_lt hslot simp [lockReleaseStmt, execIRStmt, evalIRExpr, hmod] -/-- On the reachable (binary) lock values, the Yul decision `eq(lock, 1)` -agrees with the source model's `lock ≠ 0` (`NonReentrantGuard.guarded`). -/ -theorem guard_decision_agrees (v : Nat) (hv : v = 0 ∨ v = 1) : - (v = 1) ↔ v ≠ 0 := by - rcases hv with h | h <;> simp [h] +/-- The emitted Yul and source model use the same nonzero lock decision. -/ +theorem guard_decision_agrees (v : Nat) : (v ≠ 0) ↔ v ≠ 0 := by + rfl /-- Acquire-then-release round-trips the lock slot: the transient storage function is extensionally the initial one when the slot started free. -/ diff --git a/Verity/Macro/Translate.lean b/Verity/Macro/Translate.lean index c8808d802..5d9bb4299 100644 --- a/Verity/Macro/Translate.lean +++ b/Verity/Macro/Translate.lean @@ -2402,7 +2402,13 @@ private def threadHelperApp? (toString name.getId).endsWith ("." ++ fn.name)) && fn.params.size == args.size match helper? with - | some _ => some <$> helperCallWithAdv name args adv + | some helper => + let target ← + if helper.nonReentrantLock.isSome && helper.reentrancyTrusted then + mkSuffixedIdent name "_unguarded" + else + pure name + some <$> helperCallWithAdv target args adv | none => pure none private def rewriteTypedInterfaceCall? @@ -5044,6 +5050,7 @@ def validateGeneratedDefNamesPublic let helperNames := #[ s!"{generatedFnName}_modelBody" + , s!"{generatedFnName}_entrypoint" , s!"{generatedFnName}_model" , s!"{generatedFnName}_bridge" , s!"{generatedFnName}_semantic_preservation" @@ -5059,6 +5066,11 @@ def validateGeneratedDefNamesPublic , s!"{generatedFnName}_requires_role" , s!"{generatedFnName}_access_control" ] + let helperNames := + if fn.nonReentrantLock.isSome && fn.reentrancyTrusted then + helperNames.push s!"{generatedFnName}_unguarded" + else + helperNames for helperName in helperNames do if storageNames.contains helperName then throwErrorAt fn.ident @@ -5423,6 +5435,13 @@ def mkIncludeAliasCommandsPublic unless fn.isInternal do let tgt := mkIdent (mixinName ++ fn.ident.getId) cmds := cmds.push (← `(command| abbrev $(fn.ident) := $tgt)) + let predicateId ← mkSuffixedIdent fn.ident "_entrypoint" + let predicateTgt ← mkSuffixedIdent tgt "_entrypoint" + cmds := cmds.push (← `(command| abbrev $predicateId := $predicateTgt)) + if fn.nonReentrantLock.isSome && fn.reentrancyTrusted then + let unguardedId ← mkSuffixedIdent fn.ident "_unguarded" + let unguardedTgt ← mkSuffixedIdent tgt "_unguarded" + cmds := cmds.push (← `(command| abbrev $unguardedId := $unguardedTgt)) for modDecl in mixin.modifiers do unless modifierContainsExternalCallSyntaxPublic modDecl do let tgt := mkIdent (mixinName ++ modDecl.ident.getId) @@ -5542,6 +5561,15 @@ def mkFunctionCommandsPublic mkContractFnType fn.params fn.returnTy let fnExecutableBody := ⟨← threadAdversaryThroughExecutableSyntax externalDecls adversarialHelpers fn.params advTerm fnExecutableBody.raw⟩ + let mut extraExecutableCmds : Array Cmd := #[] + if fn.nonReentrantLock.isSome && fn.reentrancyTrusted then + let unguardedId ← mkSuffixedIdent fn.ident "_unguarded" + let unguardedValue ← if opensReentrancyWindow then + mkContractFnValueWithAdversary advIdent fn.params fnExecutableBody + else + mkContractFnValue fn.params fnExecutableBody + extraExecutableCmds := extraExecutableCmds.push + (← `(command| def $unguardedId : $fnType := $unguardedValue)) let fnExecutableBody ← match fn.nonReentrantLock with | some lockIdent => let lockName := toString lockIdent.getId @@ -5628,7 +5656,7 @@ def mkFunctionCommandsPublic body := $modelBodyName isInternal := $internalTerm }) - pure #[fnCmd, entrypointCmd, bodyCmd, modelCmd] + pure (extraExecutableCmds ++ #[fnCmd, entrypointCmd, bodyCmd, modelCmd]) /-- Emit the contract-wide union of all externally callable entrypoint predicates. Each per-function predicate keeps arguments existential and uses diff --git a/docs-site/public/llms.txt b/docs-site/public/llms.txt index 6cb26e71a..5aef189f7 100644 --- a/docs-site/public/llms.txt +++ b/docs-site/public/llms.txt @@ -33,7 +33,7 @@ Every transition inside the proof envelope is either fully verified or recorded - **Language**: Lean 4.31.0 - **Core Size**: 2040 lines - **Verified Contracts**: 15 (Counter, ERC20, ERC721, Ledger, LocalObligationMacroSmoke, Ownable, Owned, OwnedCounter, OwnedCounterComposed, ReentrancyExample, ReentrancyRelyGuarantee, SafeCounter, SimpleStorage, SimpleToken, Vault) -- **Theorems**: 330 across 15 categories, 330 fully proven +- **Theorems**: 329 across 15 categories, 329 fully proven - **Axioms**: 1 documented Lean axioms (see AXIOMS.md) - **Tests**: 528 Foundry tests, 239 property tests - **Build**: `lake build` verifies all proofs diff --git a/docs/VERIFICATION_STATUS.md b/docs/VERIFICATION_STATUS.md index 87a316638..08f468438 100644 --- a/docs/VERIFICATION_STATUS.md +++ b/docs/VERIFICATION_STATUS.md @@ -40,11 +40,11 @@ EVM Bytecode | ERC721 | 11 | Baseline | `Contracts/ERC721/Proofs/` | | Vault | 9 | Baseline | `Contracts/Vault/Proofs/` | | ReentrancyExample | 5 | Complete | `Contracts/ReentrancyExample/Contract.lean` | -| ReentrancyRelyGuarantee | 11 | Semantic | `Contracts/ReentrancyRelyGuarantee/Contract.lean` | +| ReentrancyRelyGuarantee | 10 | Semantic | `Contracts/ReentrancyRelyGuarantee/Contract.lean` | | CryptoHash | 0 | No specs | `Contracts/CryptoHash/Contract.lean` | -| **Total** | **330** | **✅ 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 (330 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. @@ -205,7 +205,7 @@ Also note that the macro-generated `*_semantic_preservation` theorems are not co | ERC721 | 100% (11/11) | 0 | | SafeCounter | 100% (25/25) | 0 | | ReentrancyExample | 100% (5/5) | 0 | -| ReentrancyRelyGuarantee | 0% (0/11) | 11 proof-only | +| ReentrancyRelyGuarantee | 0% (0/10) | 10 proof-only | | Ledger | 100% (33/33) | 0 | | LocalObligationMacroSmoke | 100% (4/4) | 0 | | SimpleStorage | 95% (19/20) | 1 proof-only | @@ -217,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**: 77% coverage (255/330), 75 remaining exclusions all proof-only +**Status**: 78% coverage (255/329), 74 remaining exclusions all proof-only -- **Total Properties**: 330 +- **Total Properties**: 329 - **Covered**: 255 -- **Excluded**: 75 (all proof-only) +- **Excluded**: 74 (all proof-only) **Proof-Only Properties (59 exclusions)**: Internal proof machinery that cannot be tested in Foundry. From d7cd60058b08874f179b2eaca8aeaa13368e8072 Mon Sep 17 00:00:00 2001 From: Thomas Marchand Date: Mon, 7 Sep 2026 15:26:35 +0200 Subject: [PATCH 14/50] fix(proofs): open callback bridge namespace --- Verity/Proofs/Model/GeneratedEntrypointRegistry.lean | 2 ++ 1 file changed, 2 insertions(+) diff --git a/Verity/Proofs/Model/GeneratedEntrypointRegistry.lean b/Verity/Proofs/Model/GeneratedEntrypointRegistry.lean index 709ee0020..29df6ddaf 100644 --- a/Verity/Proofs/Model/GeneratedEntrypointRegistry.lean +++ b/Verity/Proofs/Model/GeneratedEntrypointRegistry.lean @@ -46,6 +46,8 @@ theorem guardedPing_reentry_blocked (adv : AdversaryModel) (value : Uint256) end GeneratedRegistry +open Compiler.CompilationModel.DenoteExternalCalls + /-- `ReentrancyRelyGuarantee` consumes the emitted registry at the restricted callback boundary. Contract-specific preservation obligations remain with authors; this PR establishes only the generated registry/guard connection. -/ From f036c9b3dd1016c1bd547224ae1bd98c53980212 Mon Sep 17 00:00:00 2001 From: Thomas Marchand Date: Mon, 7 Sep 2026 16:54:30 +0200 Subject: [PATCH 15/50] fix(reentrancy): close helper routing gaps --- Verity/Macro/Elaborate.lean | 3 ++- Verity/Macro/Translate.lean | 49 ++++++++++++++++++++++++++++--------- 2 files changed, 39 insertions(+), 13 deletions(-) diff --git a/Verity/Macro/Elaborate.lean b/Verity/Macro/Elaborate.lean index 723755bd4..51a20f56f 100644 --- a/Verity/Macro/Elaborate.lean +++ b/Verity/Macro/Elaborate.lean @@ -85,7 +85,6 @@ private def elabVerityContractOrMixin (stx : Syntax) : CommandElabM Unit := do let isMixin := parsed.isMixin let resolvedIncludes := parsed.resolvedIncludes - validateGeneratedDefNamesPublic fields constDecls immutableDecls functions validateConstantDeclsPublic constDecls validateImmutableDeclsPublic fields constDecls immutableDecls ctor validateExternalDeclsPublic externalDecls @@ -121,6 +120,8 @@ private def elabVerityContractOrMixin (stx : Syntax) : CommandElabM Unit := do let translationExternalDecls := mixinExternalDecls ++ externalDecls let translationFunctions := mixinFunctions ++ functions let translationRoleDecls := mixinRoleDecls ++ roleDecls + validateGeneratedDefNamesPublic structDecls translationFields translationConstDecls + translationImmutableDecls (mixinModifiers ++ modifiers) translationFunctions validateFunctionDeclsPublic translationFields translationErrorDecls translationEventDecls translationConstDecls translationImmutableDecls translationExternalDecls ctor (mixinModifiers ++ modifiers) translationFunctions diff --git a/Verity/Macro/Translate.lean b/Verity/Macro/Translate.lean index 5d9bb4299..72fbb8d29 100644 --- a/Verity/Macro/Translate.lean +++ b/Verity/Macro/Translate.lean @@ -2394,13 +2394,21 @@ private def helperCallWithAdv (name : Ident) (args : Array Term) (adv : Term) : app ← `(term| $app $arg) pure app +private def helperCall (name : Ident) (args : Array Term) : CommandElabM Term := do + let mut app : Term := ⟨name.raw⟩ + for arg in args do + app ← `(term| $app $arg) + pure app + private def threadHelperApp? - (adversarialHelpers : Array FunctionDecl) (name : Ident) (args : Array Term) + (helpers : Array FunctionDecl) (adversarialHelpers : Array FunctionDecl) + (name : Ident) (args : Array Term) (adv : Term) : CommandElabM (Option Term) := do - let helper? := adversarialHelpers.find? fun fn => + let matchesHelper := fun (fn : FunctionDecl) => (fn.name == toString name.getId || fn.ident.getId == name.getId || (toString name.getId).endsWith ("." ++ fn.name)) && fn.params.size == args.size + let helper? := helpers.find? matchesHelper match helper? with | some helper => let target ← @@ -2408,7 +2416,12 @@ private def threadHelperApp? mkSuffixedIdent name "_unguarded" else pure name - some <$> helperCallWithAdv target args adv + if adversarialHelpers.any matchesHelper then + some <$> helperCallWithAdv target args adv + else if helper.nonReentrantLock.isSome && helper.reentrancyTrusted then + some <$> helperCall target args + else + pure none | none => pure none private def rewriteTypedInterfaceCall? @@ -2723,10 +2736,11 @@ private def adaptHoistedWordContext (stx : Term) : CommandElabM Term := do private partial def threadAdversaryThroughExecutableSyntax (externalDecls : Array ExternalDecl) + (helpers : Array FunctionDecl) (adversarialHelpers : Array FunctionDecl) (params : Array ParamDecl) (adv : Term) (stx : Syntax) : CommandElabM Syntax := do - let go := threadAdversaryThroughExecutableSyntax externalDecls adversarialHelpers params adv + let go := threadAdversaryThroughExecutableSyntax externalDecls helpers adversarialHelpers params adv let recurseChildren : CommandElabM Syntax := do match stx with | .node info kind args => @@ -2762,7 +2776,7 @@ private partial def threadAdversaryThroughExecutableSyntax let rewrittenBody : TSyntax ``Lean.Parser.Term.doSeq := ⟨bodyRaw⟩ pure (#[], ← `(term| do $rewrittenBody)) | `(term| $name:ident($[$args:term],*)) => - match ← threadHelperApp? adversarialHelpers name args adv with + match ← threadHelperApp? helpers adversarialHelpers name args adv with | some app => pure (#[], app) | none => let mut binds : Array (Ident × Term) := #[] @@ -2899,7 +2913,7 @@ private partial def threadAdversaryThroughExecutableSyntax (← `(doElem| let $pat:term ← (totalSupply (externalArgAddress $rewrittenToken) $adv))) | `(doElem| let $name:ident ← $fn:ident($[$args:term],*)) => - match ← threadHelperApp? adversarialHelpers fn args adv with + match ← threadHelperApp? helpers adversarialHelpers fn args adv with | some app => `(doElem| let $name ← $app:term) | none => recurseChildren | `(doElem| let $name:ident ← $fn:ident $args:term*) => @@ -2924,7 +2938,7 @@ private partial def threadAdversaryThroughExecutableSyntax wrapBinds binds (← `(doElem| let $name ← (totalSupply (externalArgAddress $token) $adv))) else - match ← threadHelperApp? adversarialHelpers fn original adv with + match ← threadHelperApp? helpers adversarialHelpers fn original adv with | some app => hoistLive false app fun rewritten => `(doElem| let $name ← $rewritten:term) | none => @@ -3051,7 +3065,7 @@ private partial def threadAdversaryThroughExecutableSyntax let rewritten ← rewriteLinkedCallTerm externalDecls params adv rhs `(doElem| $rewritten:term) | _ => recurseChildren - else match ← threadHelperApp? adversarialHelpers fn original adv with + else match ← threadHelperApp? helpers adversarialHelpers fn original adv with | some app => hoistLive false app fun rewritten => `(doElem| $rewritten:term) | none => @@ -3063,7 +3077,7 @@ private partial def threadAdversaryThroughExecutableSyntax hoistLive false stmt fun rewritten => `(doElem| $rewritten:term) | `(term| $name:ident $args:term*) => let original := args.map fun arg => (⟨arg.raw⟩ : Term) - match ← threadHelperApp? adversarialHelpers name original adv with + match ← threadHelperApp? helpers adversarialHelpers name original adv with | some app => pure app.raw | none => if isLiveStateExternalCall ⟨stx⟩ then @@ -4962,9 +4976,11 @@ def validateConstantDeclsPublic (constDecls : Array ConstantDecl) : CommandElabM validateConstantExprTypes constDecls def validateGeneratedDefNamesPublic + (structDecls : Array StructDecl) (fields : Array StorageFieldDecl) (constDecls : Array ConstantDecl) (immutableDecls : Array ImmutableDecl) + (modifiers : Array ModifierDecl) (functions : Array FunctionDecl) : CommandElabM Unit := do let reservedGeneratedNames : Array String := #["spec", "storageNamespace", "entrypointRegistry"] let mut generatedHelperNames : Array String := reservedGeneratedNames @@ -5089,6 +5105,15 @@ def validateGeneratedDefNamesPublic s!"function '{fn.name}' generates duplicate helper declaration '{helperName}'" generatedHelperNames := generatedHelperNames.push helperName + for structDecl in structDecls do + if generatedHelperNames.contains structDecl.name then + throwErrorAt structDecl.ident + s!"struct '{structDecl.name}' conflicts with generated declaration '{structDecl.name}'" + for modifierDecl in modifiers do + if generatedHelperNames.contains modifierDecl.name then + throwErrorAt modifierDecl.ident + s!"modifier '{modifierDecl.name}' conflicts with generated declaration '{modifierDecl.name}'" + def validateImmutableDeclsPublic (fields : Array StorageFieldDecl) (constDecls : Array ConstantDecl) @@ -5338,7 +5363,7 @@ def mkConstructorDefCommandPublic pure ⟨advIdent.raw⟩ else `(Compiler.CompilationModel.DenoteExternalCalls.AdversaryModel.stub) - let executableBody := ⟨← threadAdversaryThroughExecutableSyntax externalDecls adversarialHelpers + let executableBody := ⟨← threadAdversaryThroughExecutableSyntax externalDecls functions adversarialHelpers ctor.params advTerm executableBody.raw⟩ let fnType ← if opensReentrancyWindow then mkContractFnTypeWithAdversary ctor.params .unit @@ -5408,7 +5433,7 @@ def mkHostConstructorDefCommandPublic preludes := preludes.push (← `(doElem| $tgt:ident $args*)) let body ← `(term| do $[$preludes:doElem]* $[$elems:doElem]*) let executableBody ← rewriteForEachExecutableBody fields externalDecls ctor.params body - let executableBody := ⟨← threadAdversaryThroughExecutableSyntax externalDecls ownAdversarialHelpers + let executableBody := ⟨← threadAdversaryThroughExecutableSyntax externalDecls functions ownAdversarialHelpers ctor.params advTerm executableBody.raw⟩ let fnValue ← if containsExternalCall then mkContractFnValueWithAdversary advIdent ctor.params executableBody @@ -5559,7 +5584,7 @@ def mkFunctionCommandsPublic mkContractFnTypeWithAdversary fn.params fn.returnTy else mkContractFnType fn.params fn.returnTy - let fnExecutableBody := ⟨← threadAdversaryThroughExecutableSyntax externalDecls + let fnExecutableBody := ⟨← threadAdversaryThroughExecutableSyntax externalDecls functions adversarialHelpers fn.params advTerm fnExecutableBody.raw⟩ let mut extraExecutableCmds : Array Cmd := #[] if fn.nonReentrantLock.isSome && fn.reentrancyTrusted then From bb856e42a3ff92ad4209524d7616e74a931b6288 Mon Sep 17 00:00:00 2001 From: Thomas Marchand Date: Mon, 7 Sep 2026 18:49:50 +0200 Subject: [PATCH 16/50] fix(macro): preserve qualified guarded helper calls --- Contracts/Smoke/SecurityCombos.lean | 32 +++++++++++++++++++++++++++++ Verity/Macro/Translate.lean | 6 +++++- 2 files changed, 37 insertions(+), 1 deletion(-) diff --git a/Contracts/Smoke/SecurityCombos.lean b/Contracts/Smoke/SecurityCombos.lean index bd42e62e6..2ae3b2d58 100644 --- a/Contracts/Smoke/SecurityCombos.lean +++ b/Contracts/Smoke/SecurityCombos.lean @@ -175,6 +175,38 @@ verity_contract NonreentrantTrustedInternalHelperAccepted where #check_contract NonreentrantTrustedInternalHelperAccepted +-- Regression for Codex's PR #2406 qualified-helper finding. Qualified Lean +-- helpers that merely share a guarded local function's final name must retain +-- their qualifier; they do not resolve to the generated lock-free shadow. +verity_contract QualifiedHelperLibrary where + storage + + function trustedEntry (x : Uint256) : Uint256 := do + return x + + function trustedPair (x : Uint256) : Tuple [Uint256, Uint256] := do + return (x, x) + +verity_contract NonreentrantQualifiedHelperResolution where + storage + lock : Uint256 := slot 0 + + function nonreentrant(lock) reentrancy_trusted trustedEntry (x : Uint256) : Uint256 := do + return x + + function nonreentrant(lock) reentrancy_trusted trustedPair (x : Uint256) : Tuple [Uint256, Uint256] := do + return (x, x) + + function qualifiedSpace (x : Uint256) : Uint256 := do + let y ← QualifiedHelperLibrary.trustedEntry x + return y + + function qualifiedDestructure (x : Uint256) : Uint256 := do + let (left, right) ← QualifiedHelperLibrary.trustedPair x + return (add left right) + +#check_contract NonreentrantQualifiedHelperResolution + -- ════════════════════════════════════════════════════════════════════════════ -- Stress-test contracts: edge-case coverage for Language Design Axes (#1731) -- ════════════════════════════════════════════════════════════════════════════ diff --git a/Verity/Macro/Translate.lean b/Verity/Macro/Translate.lean index 72fbb8d29..47761c94a 100644 --- a/Verity/Macro/Translate.lean +++ b/Verity/Macro/Translate.lean @@ -2408,11 +2408,15 @@ private def threadHelperApp? (fn.name == toString name.getId || fn.ident.getId == name.getId || (toString name.getId).endsWith ("." ++ fn.name)) && fn.params.size == args.size + let matchesExactHelper := fun (fn : FunctionDecl) => + (fn.name == toString name.getId || fn.ident.getId == name.getId) && + fn.params.size == args.size let helper? := helpers.find? matchesHelper match helper? with | some helper => let target ← - if helper.nonReentrantLock.isSome && helper.reentrancyTrusted then + if helper.nonReentrantLock.isSome && helper.reentrancyTrusted && + matchesExactHelper helper then mkSuffixedIdent name "_unguarded" else pure name From 8ec2b67b2b5e2af5fe0e7cc83950b9bff7786b5e Mon Sep 17 00:00:00 2001 From: Thomas Marchand Date: Mon, 7 Sep 2026 19:53:51 +0200 Subject: [PATCH 17/50] test(macro): add qualified helper artifacts --- ...onreentrantQualifiedHelperResolution.t.sol | 57 +++++++++++++++++++ .../PropertyQualifiedHelperLibrary.t.sol | 39 +++++++++++++ 2 files changed, 96 insertions(+) create mode 100644 artifacts/macro_property_tests/PropertyNonreentrantQualifiedHelperResolution.t.sol create mode 100644 artifacts/macro_property_tests/PropertyQualifiedHelperLibrary.t.sol diff --git a/artifacts/macro_property_tests/PropertyNonreentrantQualifiedHelperResolution.t.sol b/artifacts/macro_property_tests/PropertyNonreentrantQualifiedHelperResolution.t.sol new file mode 100644 index 000000000..655341526 --- /dev/null +++ b/artifacts/macro_property_tests/PropertyNonreentrantQualifiedHelperResolution.t.sol @@ -0,0 +1,57 @@ +// SPDX-License-Identifier: MIT +pragma solidity ^0.8.33; + +import "./yul/YulTestBase.sol"; + +/** + * @title PropertyNonreentrantQualifiedHelperResolutionTest + * @notice Auto-generated baseline property stubs from `verity_contract` declarations. + * @dev Source: Contracts/Smoke/SecurityCombos.lean + */ +contract PropertyNonreentrantQualifiedHelperResolutionTest is YulTestBase { + address target; + address alice = address(0x1111); + + function setUp() public { + target = deployYul("NonreentrantQualifiedHelperResolution"); + require(target != address(0), "Deploy failed"); + } + + // Property 1: trustedEntry returns the direct parameter value + function testAuto_TrustedEntry_ReturnsDirectParam() public { + vm.prank(alice); + (bool ok, bytes memory ret) = target.call(abi.encodeWithSignature("trustedEntry(uint256)", uint256(1))); + require(ok, "trustedEntry reverted unexpectedly"); + assertEq(ret.length, 32, "trustedEntry ABI return length mismatch (expected 32 bytes)"); + uint256 actual = abi.decode(ret, (uint256)); + assertEq(actual, uint256(1), "trustedEntry should preserve the expected value"); + } + // Property 2: trustedPair decodes and matches the inferred tuple result + function testAuto_TrustedPair_ReturnsInferredTupleResult() public { + vm.prank(alice); + (bool ok, bytes memory ret) = target.call(abi.encodeWithSignature("trustedPair(uint256)", uint256(1))); + require(ok, "trustedPair reverted unexpectedly"); + require(ret.length >= 64, "trustedPair ABI tuple return payload unexpectedly short"); + (uint256 actual0, uint256 actual1) = abi.decode(ret, (uint256, uint256)); + assertEq(actual0, uint256(1), "trustedPair tuple element 0 should preserve the inferred result"); + assertEq(actual1, uint256(1), "trustedPair tuple element 1 should preserve the inferred result"); + } + // Property 3: TODO decode and assert `qualifiedSpace` result + function testTODO_QualifiedSpace_DecodeAndAssert() public { + vm.prank(alice); + (bool ok, bytes memory ret) = target.call(abi.encodeWithSignature("qualifiedSpace(uint256)", uint256(1))); + require(ok, "qualifiedSpace reverted unexpectedly"); + assertEq(ret.length, 32, "qualifiedSpace ABI return length mismatch (expected 32 bytes)"); + // TODO(#1011): decode `ret` and assert the concrete postcondition from Lean theorem. + ret; + } + // Property 4: TODO decode and assert `qualifiedDestructure` result + function testTODO_QualifiedDestructure_DecodeAndAssert() public { + vm.prank(alice); + (bool ok, bytes memory ret) = target.call(abi.encodeWithSignature("qualifiedDestructure(uint256)", uint256(1))); + require(ok, "qualifiedDestructure reverted unexpectedly"); + assertEq(ret.length, 32, "qualifiedDestructure 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/PropertyQualifiedHelperLibrary.t.sol b/artifacts/macro_property_tests/PropertyQualifiedHelperLibrary.t.sol new file mode 100644 index 000000000..ddd9e72e6 --- /dev/null +++ b/artifacts/macro_property_tests/PropertyQualifiedHelperLibrary.t.sol @@ -0,0 +1,39 @@ +// SPDX-License-Identifier: MIT +pragma solidity ^0.8.33; + +import "./yul/YulTestBase.sol"; + +/** + * @title PropertyQualifiedHelperLibraryTest + * @notice Auto-generated baseline property stubs from `verity_contract` declarations. + * @dev Source: Contracts/Smoke/SecurityCombos.lean + */ +contract PropertyQualifiedHelperLibraryTest is YulTestBase { + address target; + address alice = address(0x1111); + + function setUp() public { + target = deployYul("QualifiedHelperLibrary"); + require(target != address(0), "Deploy failed"); + } + + // Property 1: trustedEntry returns the direct parameter value + function testAuto_TrustedEntry_ReturnsDirectParam() public { + vm.prank(alice); + (bool ok, bytes memory ret) = target.call(abi.encodeWithSignature("trustedEntry(uint256)", uint256(1))); + require(ok, "trustedEntry reverted unexpectedly"); + assertEq(ret.length, 32, "trustedEntry ABI return length mismatch (expected 32 bytes)"); + uint256 actual = abi.decode(ret, (uint256)); + assertEq(actual, uint256(1), "trustedEntry should preserve the expected value"); + } + // Property 2: trustedPair decodes and matches the inferred tuple result + function testAuto_TrustedPair_ReturnsInferredTupleResult() public { + vm.prank(alice); + (bool ok, bytes memory ret) = target.call(abi.encodeWithSignature("trustedPair(uint256)", uint256(1))); + require(ok, "trustedPair reverted unexpectedly"); + require(ret.length >= 64, "trustedPair ABI tuple return payload unexpectedly short"); + (uint256 actual0, uint256 actual1) = abi.decode(ret, (uint256, uint256)); + assertEq(actual0, uint256(1), "trustedPair tuple element 0 should preserve the inferred result"); + assertEq(actual1, uint256(1), "trustedPair tuple element 1 should preserve the inferred result"); + } +} From 89703cc3053c837eb0f38ec73b553c7379a905f5 Mon Sep 17 00:00:00 2001 From: Thomas Marchand Date: Mon, 7 Sep 2026 21:38:34 +0200 Subject: [PATCH 18/50] fix(macro): keep adversary threading namespace-local --- Contracts/Smoke/SecurityCombos.lean | 25 ++++++++++++ Verity/Macro/Translate.lean | 2 +- ...onreentrantQualifiedHelperResolution.t.sol | 40 ++++++++++++++++++- .../PropertyQualifiedHelperLibrary.t.sol | 19 +++++++++ 4 files changed, 83 insertions(+), 3 deletions(-) diff --git a/Contracts/Smoke/SecurityCombos.lean b/Contracts/Smoke/SecurityCombos.lean index 2ae3b2d58..dab2a4925 100644 --- a/Contracts/Smoke/SecurityCombos.lean +++ b/Contracts/Smoke/SecurityCombos.lean @@ -187,16 +187,33 @@ verity_contract QualifiedHelperLibrary where function trustedPair (x : Uint256) : Tuple [Uint256, Uint256] := do return (x, x) + function adversarialEntry (x : Uint256) : Uint256 := do + return x + + function adversarialPair (x : Uint256) : Tuple [Uint256, Uint256] := do + return (x, x) + verity_contract NonreentrantQualifiedHelperResolution where storage lock : Uint256 := slot 0 + linked_externals + external echo(Uint256) -> (Uint256) + function nonreentrant(lock) reentrancy_trusted trustedEntry (x : Uint256) : Uint256 := do return x function nonreentrant(lock) reentrancy_trusted trustedPair (x : Uint256) : Tuple [Uint256, Uint256] := do return (x, x) + function nonreentrant(lock) reentrancy_trusted adversarialEntry (x : Uint256) : Uint256 := do + let echoed := externalCall "echo" [x] + return echoed + + function nonreentrant(lock) reentrancy_trusted adversarialPair (x : Uint256) : Tuple [Uint256, Uint256] := do + let echoed := externalCall "echo" [x] + return (echoed, echoed) + function qualifiedSpace (x : Uint256) : Uint256 := do let y ← QualifiedHelperLibrary.trustedEntry x return y @@ -205,6 +222,14 @@ verity_contract NonreentrantQualifiedHelperResolution where let (left, right) ← QualifiedHelperLibrary.trustedPair x return (add left right) + function qualifiedAdversarialSpace (x : Uint256) : Uint256 := do + let y ← QualifiedHelperLibrary.adversarialEntry x + return y + + function qualifiedAdversarialDestructure (x : Uint256) : Uint256 := do + let (left, right) ← QualifiedHelperLibrary.adversarialPair x + return (add left right) + #check_contract NonreentrantQualifiedHelperResolution -- ════════════════════════════════════════════════════════════════════════════ diff --git a/Verity/Macro/Translate.lean b/Verity/Macro/Translate.lean index 47761c94a..63863f3b8 100644 --- a/Verity/Macro/Translate.lean +++ b/Verity/Macro/Translate.lean @@ -2420,7 +2420,7 @@ private def threadHelperApp? mkSuffixedIdent name "_unguarded" else pure name - if adversarialHelpers.any matchesHelper then + if adversarialHelpers.any matchesExactHelper then some <$> helperCallWithAdv target args adv else if helper.nonReentrantLock.isSome && helper.reentrancyTrusted then some <$> helperCall target args diff --git a/artifacts/macro_property_tests/PropertyNonreentrantQualifiedHelperResolution.t.sol b/artifacts/macro_property_tests/PropertyNonreentrantQualifiedHelperResolution.t.sol index 655341526..a4e1791d8 100644 --- a/artifacts/macro_property_tests/PropertyNonreentrantQualifiedHelperResolution.t.sol +++ b/artifacts/macro_property_tests/PropertyNonreentrantQualifiedHelperResolution.t.sol @@ -36,7 +36,25 @@ contract PropertyNonreentrantQualifiedHelperResolutionTest is YulTestBase { assertEq(actual0, uint256(1), "trustedPair tuple element 0 should preserve the inferred result"); assertEq(actual1, uint256(1), "trustedPair tuple element 1 should preserve the inferred result"); } - // Property 3: TODO decode and assert `qualifiedSpace` result + // Property 3: TODO decode and assert `adversarialEntry` result + function testTODO_AdversarialEntry_DecodeAndAssert() public { + vm.prank(alice); + (bool ok, bytes memory ret) = target.call(abi.encodeWithSignature("adversarialEntry(uint256)", uint256(1))); + require(ok, "adversarialEntry reverted unexpectedly"); + assertEq(ret.length, 32, "adversarialEntry ABI return length mismatch (expected 32 bytes)"); + // TODO(#1011): decode `ret` and assert the concrete postcondition from Lean theorem. + ret; + } + // Property 4: TODO decode and assert `adversarialPair` result + function testTODO_AdversarialPair_DecodeAndAssert() public { + vm.prank(alice); + (bool ok, bytes memory ret) = target.call(abi.encodeWithSignature("adversarialPair(uint256)", uint256(1))); + require(ok, "adversarialPair reverted unexpectedly"); + require(ret.length >= 64, "adversarialPair ABI tuple return payload unexpectedly short"); + // TODO(#1011): decode `ret` and assert the concrete postcondition from Lean theorem. + ret; + } + // Property 5: TODO decode and assert `qualifiedSpace` result function testTODO_QualifiedSpace_DecodeAndAssert() public { vm.prank(alice); (bool ok, bytes memory ret) = target.call(abi.encodeWithSignature("qualifiedSpace(uint256)", uint256(1))); @@ -45,7 +63,7 @@ contract PropertyNonreentrantQualifiedHelperResolutionTest is YulTestBase { // TODO(#1011): decode `ret` and assert the concrete postcondition from Lean theorem. ret; } - // Property 4: TODO decode and assert `qualifiedDestructure` result + // Property 6: TODO decode and assert `qualifiedDestructure` result function testTODO_QualifiedDestructure_DecodeAndAssert() public { vm.prank(alice); (bool ok, bytes memory ret) = target.call(abi.encodeWithSignature("qualifiedDestructure(uint256)", uint256(1))); @@ -54,4 +72,22 @@ contract PropertyNonreentrantQualifiedHelperResolutionTest is YulTestBase { // TODO(#1011): decode `ret` and assert the concrete postcondition from Lean theorem. ret; } + // Property 7: TODO decode and assert `qualifiedAdversarialSpace` result + function testTODO_QualifiedAdversarialSpace_DecodeAndAssert() public { + vm.prank(alice); + (bool ok, bytes memory ret) = target.call(abi.encodeWithSignature("qualifiedAdversarialSpace(uint256)", uint256(1))); + require(ok, "qualifiedAdversarialSpace reverted unexpectedly"); + assertEq(ret.length, 32, "qualifiedAdversarialSpace ABI return length mismatch (expected 32 bytes)"); + // TODO(#1011): decode `ret` and assert the concrete postcondition from Lean theorem. + ret; + } + // Property 8: TODO decode and assert `qualifiedAdversarialDestructure` result + function testTODO_QualifiedAdversarialDestructure_DecodeAndAssert() public { + vm.prank(alice); + (bool ok, bytes memory ret) = target.call(abi.encodeWithSignature("qualifiedAdversarialDestructure(uint256)", uint256(1))); + require(ok, "qualifiedAdversarialDestructure reverted unexpectedly"); + assertEq(ret.length, 32, "qualifiedAdversarialDestructure 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/PropertyQualifiedHelperLibrary.t.sol b/artifacts/macro_property_tests/PropertyQualifiedHelperLibrary.t.sol index ddd9e72e6..b4ae08718 100644 --- a/artifacts/macro_property_tests/PropertyQualifiedHelperLibrary.t.sol +++ b/artifacts/macro_property_tests/PropertyQualifiedHelperLibrary.t.sol @@ -36,4 +36,23 @@ contract PropertyQualifiedHelperLibraryTest is YulTestBase { assertEq(actual0, uint256(1), "trustedPair tuple element 0 should preserve the inferred result"); assertEq(actual1, uint256(1), "trustedPair tuple element 1 should preserve the inferred result"); } + // Property 3: adversarialEntry returns the direct parameter value + function testAuto_AdversarialEntry_ReturnsDirectParam() public { + vm.prank(alice); + (bool ok, bytes memory ret) = target.call(abi.encodeWithSignature("adversarialEntry(uint256)", uint256(1))); + require(ok, "adversarialEntry reverted unexpectedly"); + assertEq(ret.length, 32, "adversarialEntry ABI return length mismatch (expected 32 bytes)"); + uint256 actual = abi.decode(ret, (uint256)); + assertEq(actual, uint256(1), "adversarialEntry should preserve the expected value"); + } + // Property 4: adversarialPair decodes and matches the inferred tuple result + function testAuto_AdversarialPair_ReturnsInferredTupleResult() public { + vm.prank(alice); + (bool ok, bytes memory ret) = target.call(abi.encodeWithSignature("adversarialPair(uint256)", uint256(1))); + require(ok, "adversarialPair reverted unexpectedly"); + require(ret.length >= 64, "adversarialPair ABI tuple return payload unexpectedly short"); + (uint256 actual0, uint256 actual1) = abi.decode(ret, (uint256, uint256)); + assertEq(actual0, uint256(1), "adversarialPair tuple element 0 should preserve the inferred result"); + assertEq(actual1, uint256(1), "adversarialPair tuple element 1 should preserve the inferred result"); + } } From 3f6dbc78eb83f594b642d94d0f4ff0243654e786 Mon Sep 17 00:00:00 2001 From: Thomas Marchand Date: Mon, 7 Sep 2026 23:23:19 +0200 Subject: [PATCH 19/50] fix(macro): resolve guarded helper overloads --- Contracts/Smoke/SecurityCombos.lean | 21 ++++++ Verity/Macro/Translate.lean | 71 +++++++++++++------ ...onreentrantQualifiedHelperResolution.t.sol | 56 +++++++++++++-- 3 files changed, 122 insertions(+), 26 deletions(-) diff --git a/Contracts/Smoke/SecurityCombos.lean b/Contracts/Smoke/SecurityCombos.lean index dab2a4925..d35b0e0c3 100644 --- a/Contracts/Smoke/SecurityCombos.lean +++ b/Contracts/Smoke/SecurityCombos.lean @@ -214,6 +214,19 @@ verity_contract NonreentrantQualifiedHelperResolution where let echoed := externalCall "echo" [x] return (echoed, echoed) + function overloadedTrusted (_who : Address) : Uint256 := do + return 0 + + function nonreentrant(lock) reentrancy_trusted overloadedTrusted (x : Uint256) : Uint256 := do + return x + + function overloadedAdversarial (_who : Address) : Uint256 := do + return 0 + + function nonreentrant(lock) reentrancy_trusted overloadedAdversarial (x : Uint256) : Uint256 := do + let echoed := externalCall "echo" [x] + return echoed + function qualifiedSpace (x : Uint256) : Uint256 := do let y ← QualifiedHelperLibrary.trustedEntry x return y @@ -230,6 +243,14 @@ verity_contract NonreentrantQualifiedHelperResolution where let (left, right) ← QualifiedHelperLibrary.adversarialPair x return (add left right) + function overloadedTrustedCaller (x : Uint256) : Unit := do + let y ← overloadedTrusted x + require (y == x) "wrong trusted overload" + + function overloadedAdversarialCaller (x : Uint256) : Unit := do + let y ← overloadedAdversarial x + require (y == x) "wrong adversarial overload" + #check_contract NonreentrantQualifiedHelperResolution -- ════════════════════════════════════════════════════════════════════════════ diff --git a/Verity/Macro/Translate.lean b/Verity/Macro/Translate.lean index 63863f3b8..37af4e28d 100644 --- a/Verity/Macro/Translate.lean +++ b/Verity/Macro/Translate.lean @@ -2401,7 +2401,11 @@ private def helperCall (name : Ident) (args : Array Term) : CommandElabM Term := pure app private def threadHelperApp? + (fields : Array StorageFieldDecl) + (constDecls : Array ConstantDecl) (immutableDecls : Array ImmutableDecl) + (externalDecls : Array ExternalDecl) (helpers : Array FunctionDecl) (adversarialHelpers : Array FunctionDecl) + (params : Array ParamDecl) (name : Ident) (args : Array Term) (adv : Term) : CommandElabM (Option Term) := do let matchesHelper := fun (fn : FunctionDecl) => @@ -2411,16 +2415,30 @@ private def threadHelperApp? let matchesExactHelper := fun (fn : FunctionDecl) => (fn.name == toString name.getId || fn.ident.getId == name.getId) && fn.params.size == args.size - let helper? := helpers.find? matchesHelper + let exactCandidates := helpers.filter matchesExactHelper + let helper? ← + if exactCandidates.size <= 1 then + pure (exactCandidates[0]? <|> helpers.find? matchesHelper) + else + let app ← helperCall name args + try + pure ((← resolveLocalFunctionApp? fields constDecls immutableDecls externalDecls + helpers params #[] app).map (·.1)) + catch _ => + -- Validation reports ill-typed or ambiguous calls. If local binders keep + -- the argument types unavailable here, leave the original call intact + -- instead of selecting an overload by declaration order. + pure none match helper? with | some helper => let target ← if helper.nonReentrantLock.isSome && helper.reentrancyTrusted && matchesExactHelper helper then - mkSuffixedIdent name "_unguarded" + mkSuffixedIdent helper.ident "_unguarded" else pure name - if adversarialHelpers.any matchesExactHelper then + if matchesExactHelper helper && adversarialHelpers.any (fun candidate => + functionSignatureKey candidate == functionSignatureKey helper) then some <$> helperCallWithAdv target args adv else if helper.nonReentrantLock.isSome && helper.reentrancyTrusted then some <$> helperCall target args @@ -2739,12 +2757,15 @@ private def adaptHoistedWordContext (stx : Term) : CommandElabM Term := do | _ => pure stx private partial def threadAdversaryThroughExecutableSyntax + (fields : Array StorageFieldDecl) + (constDecls : Array ConstantDecl) (immutableDecls : Array ImmutableDecl) (externalDecls : Array ExternalDecl) (helpers : Array FunctionDecl) (adversarialHelpers : Array FunctionDecl) (params : Array ParamDecl) (adv : Term) (stx : Syntax) : CommandElabM Syntax := do - let go := threadAdversaryThroughExecutableSyntax externalDecls helpers adversarialHelpers params adv + let go := threadAdversaryThroughExecutableSyntax fields constDecls immutableDecls + externalDecls helpers adversarialHelpers params adv let recurseChildren : CommandElabM Syntax := do match stx with | .node info kind args => @@ -2780,7 +2801,8 @@ private partial def threadAdversaryThroughExecutableSyntax let rewrittenBody : TSyntax ``Lean.Parser.Term.doSeq := ⟨bodyRaw⟩ pure (#[], ← `(term| do $rewrittenBody)) | `(term| $name:ident($[$args:term],*)) => - match ← threadHelperApp? helpers adversarialHelpers name args adv with + match ← threadHelperApp? fields constDecls immutableDecls externalDecls + helpers adversarialHelpers params name args adv with | some app => pure (#[], app) | none => let mut binds : Array (Ident × Term) := #[] @@ -2917,7 +2939,8 @@ private partial def threadAdversaryThroughExecutableSyntax (← `(doElem| let $pat:term ← (totalSupply (externalArgAddress $rewrittenToken) $adv))) | `(doElem| let $name:ident ← $fn:ident($[$args:term],*)) => - match ← threadHelperApp? helpers adversarialHelpers fn args adv with + match ← threadHelperApp? fields constDecls immutableDecls externalDecls + helpers adversarialHelpers params fn args adv with | some app => `(doElem| let $name ← $app:term) | none => recurseChildren | `(doElem| let $name:ident ← $fn:ident $args:term*) => @@ -2942,7 +2965,8 @@ private partial def threadAdversaryThroughExecutableSyntax wrapBinds binds (← `(doElem| let $name ← (totalSupply (externalArgAddress $token) $adv))) else - match ← threadHelperApp? helpers adversarialHelpers fn original adv with + match ← threadHelperApp? fields constDecls immutableDecls externalDecls + helpers adversarialHelpers params fn original adv with | some app => hoistLive false app fun rewritten => `(doElem| let $name ← $rewritten:term) | none => @@ -3069,19 +3093,22 @@ private partial def threadAdversaryThroughExecutableSyntax let rewritten ← rewriteLinkedCallTerm externalDecls params adv rhs `(doElem| $rewritten:term) | _ => recurseChildren - else match ← threadHelperApp? helpers adversarialHelpers fn original adv with - | some app => - hoistLive false app fun rewritten => `(doElem| $rewritten:term) - | none => - match stx with - | `(doElem| $stmt:term) => - hoistLive false stmt fun rewritten => `(doElem| $rewritten:term) - | _ => recurseChildren + else + match ← threadHelperApp? fields constDecls immutableDecls externalDecls + helpers adversarialHelpers params fn original adv with + | some app => + hoistLive false app fun rewritten => `(doElem| $rewritten:term) + | none => + match stx with + | `(doElem| $stmt:term) => + hoistLive false stmt fun rewritten => `(doElem| $rewritten:term) + | _ => recurseChildren | `(doElem| $stmt:term) => hoistLive false stmt fun rewritten => `(doElem| $rewritten:term) | `(term| $name:ident $args:term*) => let original := args.map fun arg => (⟨arg.raw⟩ : Term) - match ← threadHelperApp? helpers adversarialHelpers name original adv with + match ← threadHelperApp? fields constDecls immutableDecls externalDecls + helpers adversarialHelpers params name original adv with | some app => pure app.raw | none => if isLiveStateExternalCall ⟨stx⟩ then @@ -5367,8 +5394,8 @@ def mkConstructorDefCommandPublic pure ⟨advIdent.raw⟩ else `(Compiler.CompilationModel.DenoteExternalCalls.AdversaryModel.stub) - let executableBody := ⟨← threadAdversaryThroughExecutableSyntax externalDecls functions adversarialHelpers - ctor.params advTerm executableBody.raw⟩ + let executableBody := ⟨← threadAdversaryThroughExecutableSyntax fields constDecls immutableDecls + externalDecls functions adversarialHelpers ctor.params advTerm executableBody.raw⟩ let fnType ← if opensReentrancyWindow then mkContractFnTypeWithAdversary ctor.params .unit else @@ -5437,8 +5464,8 @@ def mkHostConstructorDefCommandPublic preludes := preludes.push (← `(doElem| $tgt:ident $args*)) let body ← `(term| do $[$preludes:doElem]* $[$elems:doElem]*) let executableBody ← rewriteForEachExecutableBody fields externalDecls ctor.params body - let executableBody := ⟨← threadAdversaryThroughExecutableSyntax externalDecls functions ownAdversarialHelpers - ctor.params advTerm executableBody.raw⟩ + let executableBody := ⟨← threadAdversaryThroughExecutableSyntax fields constDecls immutableDecls + externalDecls functions ownAdversarialHelpers ctor.params advTerm executableBody.raw⟩ let fnValue ← if containsExternalCall then mkContractFnValueWithAdversary advIdent ctor.params executableBody else @@ -5588,8 +5615,8 @@ def mkFunctionCommandsPublic mkContractFnTypeWithAdversary fn.params fn.returnTy else mkContractFnType fn.params fn.returnTy - let fnExecutableBody := ⟨← threadAdversaryThroughExecutableSyntax externalDecls functions - adversarialHelpers fn.params advTerm fnExecutableBody.raw⟩ + let fnExecutableBody := ⟨← threadAdversaryThroughExecutableSyntax fields constDecls immutableDecls + externalDecls functions adversarialHelpers fn.params advTerm fnExecutableBody.raw⟩ let mut extraExecutableCmds : Array Cmd := #[] if fn.nonReentrantLock.isSome && fn.reentrancyTrusted then let unguardedId ← mkSuffixedIdent fn.ident "_unguarded" diff --git a/artifacts/macro_property_tests/PropertyNonreentrantQualifiedHelperResolution.t.sol b/artifacts/macro_property_tests/PropertyNonreentrantQualifiedHelperResolution.t.sol index a4e1791d8..e9ced0f2e 100644 --- a/artifacts/macro_property_tests/PropertyNonreentrantQualifiedHelperResolution.t.sol +++ b/artifacts/macro_property_tests/PropertyNonreentrantQualifiedHelperResolution.t.sol @@ -54,7 +54,43 @@ contract PropertyNonreentrantQualifiedHelperResolutionTest is YulTestBase { // TODO(#1011): decode `ret` and assert the concrete postcondition from Lean theorem. ret; } - // Property 5: TODO decode and assert `qualifiedSpace` result + // Property 5: overloadedTrusted returns the declared constant result + function testAuto_OverloadedTrusted_ReturnsDeclaredConstant() public { + vm.prank(alice); + (bool ok, bytes memory ret) = target.call(abi.encodeWithSignature("overloadedTrusted(address)", alice)); + require(ok, "overloadedTrusted reverted unexpectedly"); + assertEq(ret.length, 32, "overloadedTrusted ABI return length mismatch (expected 32 bytes)"); + uint256 actual = abi.decode(ret, (uint256)); + assertEq(actual, 0, "overloadedTrusted should return the declared constant"); + } + // Property 6: overloadedTrusted returns the direct parameter value + function testAuto_OverloadedTrusted_ReturnsDirectParam() public { + vm.prank(alice); + (bool ok, bytes memory ret) = target.call(abi.encodeWithSignature("overloadedTrusted(uint256)", uint256(1))); + require(ok, "overloadedTrusted reverted unexpectedly"); + assertEq(ret.length, 32, "overloadedTrusted ABI return length mismatch (expected 32 bytes)"); + uint256 actual = abi.decode(ret, (uint256)); + assertEq(actual, uint256(1), "overloadedTrusted should preserve the expected value"); + } + // Property 7: overloadedAdversarial returns the declared constant result + function testAuto_OverloadedAdversarial_ReturnsDeclaredConstant() public { + vm.prank(alice); + (bool ok, bytes memory ret) = target.call(abi.encodeWithSignature("overloadedAdversarial(address)", alice)); + require(ok, "overloadedAdversarial reverted unexpectedly"); + assertEq(ret.length, 32, "overloadedAdversarial ABI return length mismatch (expected 32 bytes)"); + uint256 actual = abi.decode(ret, (uint256)); + assertEq(actual, 0, "overloadedAdversarial should return the declared constant"); + } + // Property 8: TODO decode and assert `overloadedAdversarial` result + function testTODO_OverloadedAdversarial_DecodeAndAssert() public { + vm.prank(alice); + (bool ok, bytes memory ret) = target.call(abi.encodeWithSignature("overloadedAdversarial(uint256)", uint256(1))); + require(ok, "overloadedAdversarial reverted unexpectedly"); + assertEq(ret.length, 32, "overloadedAdversarial ABI return length mismatch (expected 32 bytes)"); + // TODO(#1011): decode `ret` and assert the concrete postcondition from Lean theorem. + ret; + } + // Property 9: TODO decode and assert `qualifiedSpace` result function testTODO_QualifiedSpace_DecodeAndAssert() public { vm.prank(alice); (bool ok, bytes memory ret) = target.call(abi.encodeWithSignature("qualifiedSpace(uint256)", uint256(1))); @@ -63,7 +99,7 @@ contract PropertyNonreentrantQualifiedHelperResolutionTest is YulTestBase { // TODO(#1011): decode `ret` and assert the concrete postcondition from Lean theorem. ret; } - // Property 6: TODO decode and assert `qualifiedDestructure` result + // Property 10: TODO decode and assert `qualifiedDestructure` result function testTODO_QualifiedDestructure_DecodeAndAssert() public { vm.prank(alice); (bool ok, bytes memory ret) = target.call(abi.encodeWithSignature("qualifiedDestructure(uint256)", uint256(1))); @@ -72,7 +108,7 @@ contract PropertyNonreentrantQualifiedHelperResolutionTest is YulTestBase { // TODO(#1011): decode `ret` and assert the concrete postcondition from Lean theorem. ret; } - // Property 7: TODO decode and assert `qualifiedAdversarialSpace` result + // Property 11: TODO decode and assert `qualifiedAdversarialSpace` result function testTODO_QualifiedAdversarialSpace_DecodeAndAssert() public { vm.prank(alice); (bool ok, bytes memory ret) = target.call(abi.encodeWithSignature("qualifiedAdversarialSpace(uint256)", uint256(1))); @@ -81,7 +117,7 @@ contract PropertyNonreentrantQualifiedHelperResolutionTest is YulTestBase { // TODO(#1011): decode `ret` and assert the concrete postcondition from Lean theorem. ret; } - // Property 8: TODO decode and assert `qualifiedAdversarialDestructure` result + // Property 12: TODO decode and assert `qualifiedAdversarialDestructure` result function testTODO_QualifiedAdversarialDestructure_DecodeAndAssert() public { vm.prank(alice); (bool ok, bytes memory ret) = target.call(abi.encodeWithSignature("qualifiedAdversarialDestructure(uint256)", uint256(1))); @@ -90,4 +126,16 @@ contract PropertyNonreentrantQualifiedHelperResolutionTest is YulTestBase { // TODO(#1011): decode `ret` and assert the concrete postcondition from Lean theorem. ret; } + // Property 13: overloadedTrustedCaller has no unexpected revert + function testAuto_OverloadedTrustedCaller_NoUnexpectedRevert() public { + vm.prank(alice); + (bool ok,) = target.call(abi.encodeWithSignature("overloadedTrustedCaller(uint256)", uint256(1))); + require(ok, "overloadedTrustedCaller reverted unexpectedly"); + } + // Property 14: overloadedAdversarialCaller has no unexpected revert + function testAuto_OverloadedAdversarialCaller_NoUnexpectedRevert() public { + vm.prank(alice); + (bool ok,) = target.call(abi.encodeWithSignature("overloadedAdversarialCaller(uint256)", uint256(1))); + require(ok, "overloadedAdversarialCaller reverted unexpectedly"); + } } From e55fba5f8a42935818130037ae4d9eed39014ce0 Mon Sep 17 00:00:00 2001 From: Thomas Marchand Date: Tue, 8 Sep 2026 01:33:20 +0200 Subject: [PATCH 20/50] fix(macro): resolve helper calls through typed locals --- Contracts/Smoke/SecurityCombos.lean | 11 ++++ Verity/Macro/Translate.lean | 52 +++++++++++++++---- ...onreentrantQualifiedHelperResolution.t.sol | 12 +++++ 3 files changed, 64 insertions(+), 11 deletions(-) diff --git a/Contracts/Smoke/SecurityCombos.lean b/Contracts/Smoke/SecurityCombos.lean index d35b0e0c3..f6be31170 100644 --- a/Contracts/Smoke/SecurityCombos.lean +++ b/Contracts/Smoke/SecurityCombos.lean @@ -196,6 +196,7 @@ verity_contract QualifiedHelperLibrary where verity_contract NonreentrantQualifiedHelperResolution where storage lock : Uint256 := slot 0 + value : Uint256 := slot 1 linked_externals external echo(Uint256) -> (Uint256) @@ -251,6 +252,16 @@ verity_contract NonreentrantQualifiedHelperResolution where let y ← overloadedAdversarial x require (y == x) "wrong adversarial overload" + function overloadedTrustedLocalCaller () : Unit := do + let x ← getStorage value + let y ← overloadedTrusted x + require (y == x) "wrong trusted local overload" + + function overloadedAdversarialLocalCaller () : Unit := do + let x ← getStorage value + let y ← overloadedAdversarial x + require (y == x) "wrong adversarial local overload" + #check_contract NonreentrantQualifiedHelperResolution -- ════════════════════════════════════════════════════════════════════════════ diff --git a/Verity/Macro/Translate.lean b/Verity/Macro/Translate.lean index 37af4e28d..f9103f2a5 100644 --- a/Verity/Macro/Translate.lean +++ b/Verity/Macro/Translate.lean @@ -2405,7 +2405,7 @@ private def threadHelperApp? (constDecls : Array ConstantDecl) (immutableDecls : Array ImmutableDecl) (externalDecls : Array ExternalDecl) (helpers : Array FunctionDecl) (adversarialHelpers : Array FunctionDecl) - (params : Array ParamDecl) + (params : Array ParamDecl) (locals : Array TypedLocal) (name : Ident) (args : Array Term) (adv : Term) : CommandElabM (Option Term) := do let matchesHelper := fun (fn : FunctionDecl) => @@ -2423,7 +2423,7 @@ private def threadHelperApp? let app ← helperCall name args try pure ((← resolveLocalFunctionApp? fields constDecls immutableDecls externalDecls - helpers params #[] app).map (·.1)) + helpers params locals app).map (·.1)) catch _ => -- Validation reports ill-typed or ambiguous calls. If local binders keep -- the argument types unavailable here, leave the original call intact @@ -2763,9 +2763,10 @@ private partial def threadAdversaryThroughExecutableSyntax (helpers : Array FunctionDecl) (adversarialHelpers : Array FunctionDecl) (params : Array ParamDecl) + (locals : Array TypedLocal) (adv : Term) (stx : Syntax) : CommandElabM Syntax := do let go := threadAdversaryThroughExecutableSyntax fields constDecls immutableDecls - externalDecls helpers adversarialHelpers params adv + externalDecls helpers adversarialHelpers params locals adv let recurseChildren : CommandElabM Syntax := do match stx with | .node info kind args => @@ -2776,6 +2777,26 @@ private partial def threadAdversaryThroughExecutableSyntax let freshExternalIdent (origin : Term) : CommandElabM Ident := Lean.Elab.Term.mkFreshIdent (mkIdentFrom origin.raw (Name.mkSimple "__verity_ext")).raw + let extendLocals (scope : Array TypedLocal) (elem : TSyntax `doElem) : CommandElabM (Array TypedLocal) := do + let infer (name : Ident) (rhs : Term) := do + try + let ty ← + match ← resolveLocalFunctionApp? fields constDecls immutableDecls externalDecls + helpers params scope rhs with + | some (helper, _) => pure helper.returnTy + | none => inferPureExprType fields constDecls immutableDecls externalDecls params scope rhs + pure (scope.push (mkTypedLocal (toString name.getId) ty)) + catch _ => pure scope + match elem with + | `(doElem| let $name:ident := $rhs:term) => infer name rhs + | `(doElem| let mut $name:ident := $rhs:term) => infer name rhs + | `(doElem| let $name:ident ← $rhs:term) => + try + let ty ← inferBindSourceType fields constDecls immutableDecls externalDecls + helpers params scope rhs + pure (scope.push (mkTypedLocal (toString name.getId) ty)) + catch _ => pure scope + | _ => pure scope let rec hoistNested (bindSelf : Bool) (t : Term) : CommandElabM (Array (Ident × Term) × Term) := do let bindCall (binds : Array (Ident × Term)) (call : Term) : @@ -2802,7 +2823,7 @@ private partial def threadAdversaryThroughExecutableSyntax pure (#[], ← `(term| do $rewrittenBody)) | `(term| $name:ident($[$args:term],*)) => match ← threadHelperApp? fields constDecls immutableDecls externalDecls - helpers adversarialHelpers params name args adv with + helpers adversarialHelpers params locals name args adv with | some app => pure (#[], app) | none => let mut binds : Array (Ident × Term) := #[] @@ -2889,6 +2910,15 @@ private partial def threadAdversaryThroughExecutableSyntax let rest ← if outerWasBound then pureBinding pureValue else monadic rewritten wrapBinds binds rest match stx with + | `(doSeq| $[$elems:doElem]*) => + let mut scope := locals + let mut rewritten : Array (TSyntax `doElem) := #[] + for elem in elems do + let raw ← threadAdversaryThroughExecutableSyntax fields constDecls immutableDecls + externalDecls helpers adversarialHelpers params scope adv elem.raw + rewritten := rewritten.push ⟨raw⟩ + scope ← extendLocals scope elem + `(doSeq| $[$rewritten:doElem]*) | `(doElem| let $pat:term ← tryExternalCall $name:term [ $[$args:term],* ]) => let mut binds : Array (Ident × Term) := #[] let mut rewrittenArgs : Array Term := #[] @@ -2940,7 +2970,7 @@ private partial def threadAdversaryThroughExecutableSyntax (externalArgAddress $rewrittenToken) $adv))) | `(doElem| let $name:ident ← $fn:ident($[$args:term],*)) => match ← threadHelperApp? fields constDecls immutableDecls externalDecls - helpers adversarialHelpers params fn args adv with + helpers adversarialHelpers params locals fn args adv with | some app => `(doElem| let $name ← $app:term) | none => recurseChildren | `(doElem| let $name:ident ← $fn:ident $args:term*) => @@ -2966,7 +2996,7 @@ private partial def threadAdversaryThroughExecutableSyntax (← `(doElem| let $name ← (totalSupply (externalArgAddress $token) $adv))) else match ← threadHelperApp? fields constDecls immutableDecls externalDecls - helpers adversarialHelpers params fn original adv with + helpers adversarialHelpers params locals fn original adv with | some app => hoistLive false app fun rewritten => `(doElem| let $name ← $rewritten:term) | none => @@ -3095,7 +3125,7 @@ private partial def threadAdversaryThroughExecutableSyntax | _ => recurseChildren else match ← threadHelperApp? fields constDecls immutableDecls externalDecls - helpers adversarialHelpers params fn original adv with + helpers adversarialHelpers params locals fn original adv with | some app => hoistLive false app fun rewritten => `(doElem| $rewritten:term) | none => @@ -3108,7 +3138,7 @@ private partial def threadAdversaryThroughExecutableSyntax | `(term| $name:ident $args:term*) => let original := args.map fun arg => (⟨arg.raw⟩ : Term) match ← threadHelperApp? fields constDecls immutableDecls externalDecls - helpers adversarialHelpers params name original adv with + helpers adversarialHelpers params locals name original adv with | some app => pure app.raw | none => if isLiveStateExternalCall ⟨stx⟩ then @@ -5395,7 +5425,7 @@ def mkConstructorDefCommandPublic else `(Compiler.CompilationModel.DenoteExternalCalls.AdversaryModel.stub) let executableBody := ⟨← threadAdversaryThroughExecutableSyntax fields constDecls immutableDecls - externalDecls functions adversarialHelpers ctor.params advTerm executableBody.raw⟩ + externalDecls functions adversarialHelpers ctor.params #[] advTerm executableBody.raw⟩ let fnType ← if opensReentrancyWindow then mkContractFnTypeWithAdversary ctor.params .unit else @@ -5465,7 +5495,7 @@ def mkHostConstructorDefCommandPublic let body ← `(term| do $[$preludes:doElem]* $[$elems:doElem]*) let executableBody ← rewriteForEachExecutableBody fields externalDecls ctor.params body let executableBody := ⟨← threadAdversaryThroughExecutableSyntax fields constDecls immutableDecls - externalDecls functions ownAdversarialHelpers ctor.params advTerm executableBody.raw⟩ + externalDecls functions ownAdversarialHelpers ctor.params #[] advTerm executableBody.raw⟩ let fnValue ← if containsExternalCall then mkContractFnValueWithAdversary advIdent ctor.params executableBody else @@ -5616,7 +5646,7 @@ def mkFunctionCommandsPublic else mkContractFnType fn.params fn.returnTy let fnExecutableBody := ⟨← threadAdversaryThroughExecutableSyntax fields constDecls immutableDecls - externalDecls functions adversarialHelpers fn.params advTerm fnExecutableBody.raw⟩ + externalDecls functions adversarialHelpers fn.params #[] advTerm fnExecutableBody.raw⟩ let mut extraExecutableCmds : Array Cmd := #[] if fn.nonReentrantLock.isSome && fn.reentrancyTrusted then let unguardedId ← mkSuffixedIdent fn.ident "_unguarded" diff --git a/artifacts/macro_property_tests/PropertyNonreentrantQualifiedHelperResolution.t.sol b/artifacts/macro_property_tests/PropertyNonreentrantQualifiedHelperResolution.t.sol index e9ced0f2e..a6a3beee6 100644 --- a/artifacts/macro_property_tests/PropertyNonreentrantQualifiedHelperResolution.t.sol +++ b/artifacts/macro_property_tests/PropertyNonreentrantQualifiedHelperResolution.t.sol @@ -138,4 +138,16 @@ contract PropertyNonreentrantQualifiedHelperResolutionTest is YulTestBase { (bool ok,) = target.call(abi.encodeWithSignature("overloadedAdversarialCaller(uint256)", uint256(1))); require(ok, "overloadedAdversarialCaller reverted unexpectedly"); } + // Property 15: overloadedTrustedLocalCaller has no unexpected revert + function testAuto_OverloadedTrustedLocalCaller_NoUnexpectedRevert() public { + vm.prank(alice); + (bool ok,) = target.call(abi.encodeWithSignature("overloadedTrustedLocalCaller()")); + require(ok, "overloadedTrustedLocalCaller reverted unexpectedly"); + } + // Property 16: overloadedAdversarialLocalCaller has no unexpected revert + function testAuto_OverloadedAdversarialLocalCaller_NoUnexpectedRevert() public { + vm.prank(alice); + (bool ok,) = target.call(abi.encodeWithSignature("overloadedAdversarialLocalCaller()")); + require(ok, "overloadedAdversarialLocalCaller reverted unexpectedly"); + } } From 22ee53c7bf7aca52e2ec1e009151a1895abd0db6 Mon Sep 17 00:00:00 2001 From: Thomas Marchand Date: Tue, 8 Sep 2026 04:09:58 +0200 Subject: [PATCH 21/50] fix(macro): track tuple locals for helper overloads --- Contracts/Smoke/SecurityCombos.lean | 13 +++++ Verity/Macro/Translate.lean | 49 +++++++++++++++---- ...onreentrantQualifiedHelperResolution.t.sol | 38 +++++++++++--- 3 files changed, 82 insertions(+), 18 deletions(-) diff --git a/Contracts/Smoke/SecurityCombos.lean b/Contracts/Smoke/SecurityCombos.lean index f6be31170..2f2c95ace 100644 --- a/Contracts/Smoke/SecurityCombos.lean +++ b/Contracts/Smoke/SecurityCombos.lean @@ -228,6 +228,9 @@ verity_contract NonreentrantQualifiedHelperResolution where let echoed := externalCall "echo" [x] return echoed + function makePair (x : Uint256) : Tuple [Uint256, Uint256] := do + return (x, x) + function qualifiedSpace (x : Uint256) : Uint256 := do let y ← QualifiedHelperLibrary.trustedEntry x return y @@ -262,6 +265,16 @@ verity_contract NonreentrantQualifiedHelperResolution where let y ← overloadedAdversarial x require (y == x) "wrong adversarial local overload" + function overloadedTrustedTupleLocalCaller (x : Uint256) : Unit := do + let (left, _right) ← makePair x + let y ← overloadedTrusted left + require (y == x) "wrong trusted tuple-local overload" + + function overloadedAdversarialTupleLocalCaller (x : Uint256) : Unit := do + let (_left, right) ← makePair x + let y ← overloadedAdversarial right + require (y == x) "wrong adversarial tuple-local overload" + #check_contract NonreentrantQualifiedHelperResolution -- ════════════════════════════════════════════════════════════════════════════ diff --git a/Verity/Macro/Translate.lean b/Verity/Macro/Translate.lean index f9103f2a5..5bc7c234d 100644 --- a/Verity/Macro/Translate.lean +++ b/Verity/Macro/Translate.lean @@ -2787,16 +2787,45 @@ private partial def threadAdversaryThroughExecutableSyntax | none => inferPureExprType fields constDecls immutableDecls externalDecls params scope rhs pure (scope.push (mkTypedLocal (toString name.getId) ty)) catch _ => pure scope - match elem with - | `(doElem| let $name:ident := $rhs:term) => infer name rhs - | `(doElem| let mut $name:ident := $rhs:term) => infer name rhs - | `(doElem| let $name:ident ← $rhs:term) => - try - let ty ← inferBindSourceType fields constDecls immutableDecls externalDecls - helpers params scope rhs - pure (scope.push (mkTypedLocal (toString name.getId) ty)) - catch _ => pure scope - | _ => pure scope + let inferTuple (names : Array (Option String)) (rhs : Term) := do + try + match ← inferTupleSourceTypes? fields constDecls immutableDecls externalDecls + helpers params scope rhs with + | some valueTys => + if names.size != valueTys.size then + pure scope + else + let typedNames := (names.zip valueTys).filterMap fun (name?, ty) => + name?.map (fun name => mkTypedLocal name ty) + pure (scope ++ typedNames) + | none => pure scope + catch _ => pure scope + let tupleScope? ← do + let stx := elem.raw + if stx.getKind == `Lean.Parser.Term.doLet then + let patDecl := stx[3][0] + match tupleBinderNames? patDecl[0] with + | some names => pure (some (← inferTuple names ⟨patDecl[4]⟩)) + | none => pure none + else if stx.getKind == `Lean.Parser.Term.doLetArrow then + let patDecl := stx[3] + match tupleBinderNames? patDecl[0] with + | some names => pure (some (← inferTuple names ⟨patDecl[3][0]⟩)) + | none => pure none + else + pure none + match tupleScope? with + | some tupleScope => pure tupleScope + | none => match elem with + | `(doElem| let $name:ident := $rhs:term) => infer name rhs + | `(doElem| let mut $name:ident := $rhs:term) => infer name rhs + | `(doElem| let $name:ident ← $rhs:term) => + try + let ty ← inferBindSourceType fields constDecls immutableDecls externalDecls + helpers params scope rhs + pure (scope.push (mkTypedLocal (toString name.getId) ty)) + catch _ => pure scope + | _ => pure scope let rec hoistNested (bindSelf : Bool) (t : Term) : CommandElabM (Array (Ident × Term) × Term) := do let bindCall (binds : Array (Ident × Term)) (call : Term) : diff --git a/artifacts/macro_property_tests/PropertyNonreentrantQualifiedHelperResolution.t.sol b/artifacts/macro_property_tests/PropertyNonreentrantQualifiedHelperResolution.t.sol index a6a3beee6..d66bee46d 100644 --- a/artifacts/macro_property_tests/PropertyNonreentrantQualifiedHelperResolution.t.sol +++ b/artifacts/macro_property_tests/PropertyNonreentrantQualifiedHelperResolution.t.sol @@ -90,7 +90,17 @@ contract PropertyNonreentrantQualifiedHelperResolutionTest is YulTestBase { // TODO(#1011): decode `ret` and assert the concrete postcondition from Lean theorem. ret; } - // Property 9: TODO decode and assert `qualifiedSpace` result + // Property 9: makePair decodes and matches the inferred tuple result + function testAuto_MakePair_ReturnsInferredTupleResult() public { + vm.prank(alice); + (bool ok, bytes memory ret) = target.call(abi.encodeWithSignature("makePair(uint256)", uint256(1))); + require(ok, "makePair reverted unexpectedly"); + require(ret.length >= 64, "makePair ABI tuple return payload unexpectedly short"); + (uint256 actual0, uint256 actual1) = abi.decode(ret, (uint256, uint256)); + assertEq(actual0, uint256(1), "makePair tuple element 0 should preserve the inferred result"); + assertEq(actual1, uint256(1), "makePair tuple element 1 should preserve the inferred result"); + } + // Property 10: TODO decode and assert `qualifiedSpace` result function testTODO_QualifiedSpace_DecodeAndAssert() public { vm.prank(alice); (bool ok, bytes memory ret) = target.call(abi.encodeWithSignature("qualifiedSpace(uint256)", uint256(1))); @@ -99,7 +109,7 @@ contract PropertyNonreentrantQualifiedHelperResolutionTest is YulTestBase { // TODO(#1011): decode `ret` and assert the concrete postcondition from Lean theorem. ret; } - // Property 10: TODO decode and assert `qualifiedDestructure` result + // Property 11: TODO decode and assert `qualifiedDestructure` result function testTODO_QualifiedDestructure_DecodeAndAssert() public { vm.prank(alice); (bool ok, bytes memory ret) = target.call(abi.encodeWithSignature("qualifiedDestructure(uint256)", uint256(1))); @@ -108,7 +118,7 @@ contract PropertyNonreentrantQualifiedHelperResolutionTest is YulTestBase { // TODO(#1011): decode `ret` and assert the concrete postcondition from Lean theorem. ret; } - // Property 11: TODO decode and assert `qualifiedAdversarialSpace` result + // Property 12: TODO decode and assert `qualifiedAdversarialSpace` result function testTODO_QualifiedAdversarialSpace_DecodeAndAssert() public { vm.prank(alice); (bool ok, bytes memory ret) = target.call(abi.encodeWithSignature("qualifiedAdversarialSpace(uint256)", uint256(1))); @@ -117,7 +127,7 @@ contract PropertyNonreentrantQualifiedHelperResolutionTest is YulTestBase { // TODO(#1011): decode `ret` and assert the concrete postcondition from Lean theorem. ret; } - // Property 12: TODO decode and assert `qualifiedAdversarialDestructure` result + // Property 13: TODO decode and assert `qualifiedAdversarialDestructure` result function testTODO_QualifiedAdversarialDestructure_DecodeAndAssert() public { vm.prank(alice); (bool ok, bytes memory ret) = target.call(abi.encodeWithSignature("qualifiedAdversarialDestructure(uint256)", uint256(1))); @@ -126,28 +136,40 @@ contract PropertyNonreentrantQualifiedHelperResolutionTest is YulTestBase { // TODO(#1011): decode `ret` and assert the concrete postcondition from Lean theorem. ret; } - // Property 13: overloadedTrustedCaller has no unexpected revert + // Property 14: overloadedTrustedCaller has no unexpected revert function testAuto_OverloadedTrustedCaller_NoUnexpectedRevert() public { vm.prank(alice); (bool ok,) = target.call(abi.encodeWithSignature("overloadedTrustedCaller(uint256)", uint256(1))); require(ok, "overloadedTrustedCaller reverted unexpectedly"); } - // Property 14: overloadedAdversarialCaller has no unexpected revert + // Property 15: overloadedAdversarialCaller has no unexpected revert function testAuto_OverloadedAdversarialCaller_NoUnexpectedRevert() public { vm.prank(alice); (bool ok,) = target.call(abi.encodeWithSignature("overloadedAdversarialCaller(uint256)", uint256(1))); require(ok, "overloadedAdversarialCaller reverted unexpectedly"); } - // Property 15: overloadedTrustedLocalCaller has no unexpected revert + // Property 16: overloadedTrustedLocalCaller has no unexpected revert function testAuto_OverloadedTrustedLocalCaller_NoUnexpectedRevert() public { vm.prank(alice); (bool ok,) = target.call(abi.encodeWithSignature("overloadedTrustedLocalCaller()")); require(ok, "overloadedTrustedLocalCaller reverted unexpectedly"); } - // Property 16: overloadedAdversarialLocalCaller has no unexpected revert + // Property 17: overloadedAdversarialLocalCaller has no unexpected revert function testAuto_OverloadedAdversarialLocalCaller_NoUnexpectedRevert() public { vm.prank(alice); (bool ok,) = target.call(abi.encodeWithSignature("overloadedAdversarialLocalCaller()")); require(ok, "overloadedAdversarialLocalCaller reverted unexpectedly"); } + // Property 18: overloadedTrustedTupleLocalCaller has no unexpected revert + function testAuto_OverloadedTrustedTupleLocalCaller_NoUnexpectedRevert() public { + vm.prank(alice); + (bool ok,) = target.call(abi.encodeWithSignature("overloadedTrustedTupleLocalCaller(uint256)", uint256(1))); + require(ok, "overloadedTrustedTupleLocalCaller reverted unexpectedly"); + } + // Property 19: overloadedAdversarialTupleLocalCaller has no unexpected revert + function testAuto_OverloadedAdversarialTupleLocalCaller_NoUnexpectedRevert() public { + vm.prank(alice); + (bool ok,) = target.call(abi.encodeWithSignature("overloadedAdversarialTupleLocalCaller(uint256)", uint256(1))); + require(ok, "overloadedAdversarialTupleLocalCaller reverted unexpectedly"); + } } From aa806ce13fbf2f4b7c21b4a5d3d73325e5ebd420 Mon Sep 17 00:00:00 2001 From: Thomas Marchand Date: Tue, 8 Sep 2026 07:22:29 +0200 Subject: [PATCH 22/50] fix(macro): recurse qualified calls and track loop locals --- Contracts/Smoke/SecurityCombos.lean | 14 ++++++++ Verity/Macro/Translate.lean | 7 ++++ ...onreentrantQualifiedHelperResolution.t.sol | 33 +++++++++++++++---- 3 files changed, 48 insertions(+), 6 deletions(-) diff --git a/Contracts/Smoke/SecurityCombos.lean b/Contracts/Smoke/SecurityCombos.lean index 2f2c95ace..c1c45dd68 100644 --- a/Contracts/Smoke/SecurityCombos.lean +++ b/Contracts/Smoke/SecurityCombos.lean @@ -247,6 +247,10 @@ verity_contract NonreentrantQualifiedHelperResolution where let (left, right) ← QualifiedHelperLibrary.adversarialPair x return (add left right) + function reentrancy_trusted qualifiedNestedExternal (x : Uint256) : Uint256 := do + let y ← QualifiedHelperLibrary.trustedEntry (externalCall "echo" [x]) + return y + function overloadedTrustedCaller (x : Uint256) : Unit := do let y ← overloadedTrusted x require (y == x) "wrong trusted overload" @@ -275,6 +279,16 @@ verity_contract NonreentrantQualifiedHelperResolution where let y ← overloadedAdversarial right require (y == x) "wrong adversarial tuple-local overload" + function overloadedTrustedForEachCaller () : Unit := do + forEach "i" 1 (do + let y ← overloadedTrusted i + require (y == i) "wrong trusted loop-local overload") + + function overloadedAdversarialForEachSetBitCaller () : Unit := do + forEachSetBit "i" 1 (do + let y ← overloadedAdversarial i + require (y == i) "wrong adversarial loop-local overload") + #check_contract NonreentrantQualifiedHelperResolution -- ════════════════════════════════════════════════════════════════════════════ diff --git a/Verity/Macro/Translate.lean b/Verity/Macro/Translate.lean index 5bc7c234d..7c2b6080b 100644 --- a/Verity/Macro/Translate.lean +++ b/Verity/Macro/Translate.lean @@ -2431,6 +2431,11 @@ private def threadHelperApp? pure none match helper? with | some helper => + if !matchesExactHelper helper then + -- A qualified application whose suffix happens to match a local helper + -- is not a local helper call. Leave it to the recursive traversal so + -- linked calls nested in its arguments still receive the adversary. + return none let target ← if helper.nonReentrantLock.isSome && helper.reentrancyTrusted && matchesExactHelper helper then @@ -2817,6 +2822,8 @@ private partial def threadAdversaryThroughExecutableSyntax match tupleScope? with | some tupleScope => pure tupleScope | none => match elem with + | `(doElem| let $name:ident : Uint256 := $_rhs:term) => + pure (scope.push (mkTypedLocal (toString name.getId) .uint256)) | `(doElem| let $name:ident := $rhs:term) => infer name rhs | `(doElem| let mut $name:ident := $rhs:term) => infer name rhs | `(doElem| let $name:ident ← $rhs:term) => diff --git a/artifacts/macro_property_tests/PropertyNonreentrantQualifiedHelperResolution.t.sol b/artifacts/macro_property_tests/PropertyNonreentrantQualifiedHelperResolution.t.sol index d66bee46d..94cd69b9d 100644 --- a/artifacts/macro_property_tests/PropertyNonreentrantQualifiedHelperResolution.t.sol +++ b/artifacts/macro_property_tests/PropertyNonreentrantQualifiedHelperResolution.t.sol @@ -136,40 +136,61 @@ contract PropertyNonreentrantQualifiedHelperResolutionTest is YulTestBase { // TODO(#1011): decode `ret` and assert the concrete postcondition from Lean theorem. ret; } - // Property 14: overloadedTrustedCaller has no unexpected revert + // Property 14: TODO decode and assert `qualifiedNestedExternal` result + function testTODO_QualifiedNestedExternal_DecodeAndAssert() public { + vm.prank(alice); + (bool ok, bytes memory ret) = target.call(abi.encodeWithSignature("qualifiedNestedExternal(uint256)", uint256(1))); + require(ok, "qualifiedNestedExternal reverted unexpectedly"); + assertEq(ret.length, 32, "qualifiedNestedExternal ABI return length mismatch (expected 32 bytes)"); + // TODO(#1011): decode `ret` and assert the concrete postcondition from Lean theorem. + ret; + } + // Property 15: overloadedTrustedCaller has no unexpected revert function testAuto_OverloadedTrustedCaller_NoUnexpectedRevert() public { vm.prank(alice); (bool ok,) = target.call(abi.encodeWithSignature("overloadedTrustedCaller(uint256)", uint256(1))); require(ok, "overloadedTrustedCaller reverted unexpectedly"); } - // Property 15: overloadedAdversarialCaller has no unexpected revert + // Property 16: overloadedAdversarialCaller has no unexpected revert function testAuto_OverloadedAdversarialCaller_NoUnexpectedRevert() public { vm.prank(alice); (bool ok,) = target.call(abi.encodeWithSignature("overloadedAdversarialCaller(uint256)", uint256(1))); require(ok, "overloadedAdversarialCaller reverted unexpectedly"); } - // Property 16: overloadedTrustedLocalCaller has no unexpected revert + // Property 17: overloadedTrustedLocalCaller has no unexpected revert function testAuto_OverloadedTrustedLocalCaller_NoUnexpectedRevert() public { vm.prank(alice); (bool ok,) = target.call(abi.encodeWithSignature("overloadedTrustedLocalCaller()")); require(ok, "overloadedTrustedLocalCaller reverted unexpectedly"); } - // Property 17: overloadedAdversarialLocalCaller has no unexpected revert + // Property 18: overloadedAdversarialLocalCaller has no unexpected revert function testAuto_OverloadedAdversarialLocalCaller_NoUnexpectedRevert() public { vm.prank(alice); (bool ok,) = target.call(abi.encodeWithSignature("overloadedAdversarialLocalCaller()")); require(ok, "overloadedAdversarialLocalCaller reverted unexpectedly"); } - // Property 18: overloadedTrustedTupleLocalCaller has no unexpected revert + // Property 19: overloadedTrustedTupleLocalCaller has no unexpected revert function testAuto_OverloadedTrustedTupleLocalCaller_NoUnexpectedRevert() public { vm.prank(alice); (bool ok,) = target.call(abi.encodeWithSignature("overloadedTrustedTupleLocalCaller(uint256)", uint256(1))); require(ok, "overloadedTrustedTupleLocalCaller reverted unexpectedly"); } - // Property 19: overloadedAdversarialTupleLocalCaller has no unexpected revert + // Property 20: overloadedAdversarialTupleLocalCaller has no unexpected revert function testAuto_OverloadedAdversarialTupleLocalCaller_NoUnexpectedRevert() public { vm.prank(alice); (bool ok,) = target.call(abi.encodeWithSignature("overloadedAdversarialTupleLocalCaller(uint256)", uint256(1))); require(ok, "overloadedAdversarialTupleLocalCaller reverted unexpectedly"); } + // Property 21: overloadedTrustedForEachCaller has no unexpected revert + function testAuto_OverloadedTrustedForEachCaller_NoUnexpectedRevert() public { + vm.prank(alice); + (bool ok,) = target.call(abi.encodeWithSignature("overloadedTrustedForEachCaller()")); + require(ok, "overloadedTrustedForEachCaller reverted unexpectedly"); + } + // Property 22: overloadedAdversarialForEachSetBitCaller has no unexpected revert + function testAuto_OverloadedAdversarialForEachSetBitCaller_NoUnexpectedRevert() public { + vm.prank(alice); + (bool ok,) = target.call(abi.encodeWithSignature("overloadedAdversarialForEachSetBitCaller()")); + require(ok, "overloadedAdversarialForEachSetBitCaller reverted unexpectedly"); + } } From e5c550598c3aa5f325e8107df98e347377ed5467 Mon Sep 17 00:00:00 2001 From: Thomas Marchand Date: Tue, 8 Sep 2026 08:41:02 +0200 Subject: [PATCH 23/50] fix(reentrancy): complete registry executable semantics --- Contracts/Smoke/SecurityCombos.lean | 18 ++ Verity/Core/Model/CallbackBridge.lean | 70 +++++++ Verity/Core/Model/NonReentrantGuard.lean | 19 +- Verity/Macro/Translate.lean | 184 +++++++++++++----- .../Model/GeneratedEntrypointRegistry.lean | 10 +- 5 files changed, 239 insertions(+), 62 deletions(-) diff --git a/Contracts/Smoke/SecurityCombos.lean b/Contracts/Smoke/SecurityCombos.lean index c1c45dd68..3a0825179 100644 --- a/Contracts/Smoke/SecurityCombos.lean +++ b/Contracts/Smoke/SecurityCombos.lean @@ -251,6 +251,10 @@ verity_contract NonreentrantQualifiedHelperResolution where let y ← QualifiedHelperLibrary.trustedEntry (externalCall "echo" [x]) return y + function reentrancy_trusted trustedNestedExternal (x : Uint256) : Uint256 := do + let y ← trustedEntry(externalCall "echo" [x]) + return y + function overloadedTrustedCaller (x : Uint256) : Unit := do let y ← overloadedTrusted x require (y == x) "wrong trusted overload" @@ -279,6 +283,20 @@ verity_contract NonreentrantQualifiedHelperResolution where let y ← overloadedAdversarial right require (y == x) "wrong adversarial tuple-local overload" + function overloadedTrustedQualifiedTupleCaller (x : Uint256) : Unit := do + let (left, _right) ← QualifiedHelperLibrary.trustedPair x + let y ← overloadedTrusted left + require (y == x) "wrong qualified tuple-local overload" + + function nonreentrant(lock) reentrancy_trusted staticResultControlsStorage + (target : Uint256, x : Uint256) + local_obligations [manual_low_level_refinement := assumed "Static-call result threading is the explicit low-level boundary under test."] : Unit := do + let observed ← evmStaticCall(50000, target, 0, 0, 0, 0) + if observed == x then + setStorage value observed + else + pure () + function overloadedTrustedForEachCaller () : Unit := do forEach "i" 1 (do let y ← overloadedTrusted i diff --git a/Verity/Core/Model/CallbackBridge.lean b/Verity/Core/Model/CallbackBridge.lean index 782c64e64..e39c305af 100644 --- a/Verity/Core/Model/CallbackBridge.lean +++ b/Verity/Core/Model/CallbackBridge.lean @@ -42,6 +42,76 @@ instance : Coe (List (Verity.ContractState → Verity.ContractState)) end EntrypointRegistry +/-- EVM frame data chosen by a callee when it calls back into the current +contract. Entrypoint arguments remain existential in the generated +registry; this record covers the ambient values observable through +`msg.sender`, `msg.value`, and raw calldata intrinsics. -/ +structure CallbackContext where + sender : Verity.Address + msgValue : Verity.Uint256 + calldataSize : Verity.Uint256 + calldata : List Nat + +/-- Execute a registered callback in its own call frame, then restore the +outer frame's ambient context while retaining the callback's contract-state +effects. -/ +def withCallbackContext (ctx : CallbackContext) (world : Verity.ContractState) : + Verity.ContractState := + { world with + sender := ctx.sender + msgValue := ctx.msgValue + calldataSize := ctx.calldataSize + calldata := ctx.calldata } + +def restoreCallbackContext (outer callbackResult : Verity.ContractState) : + Verity.ContractState := + { callbackResult with + sender := outer.sender + msgValue := outer.msgValue + calldataSize := outer.calldataSize + calldata := outer.calldata } + +def callbackTransition (ctx : CallbackContext) + (entrypoint : Verity.ContractState → Verity.ContractState) : + Verity.ContractState → Verity.ContractState := + fun outer => restoreCallbackContext outer (entrypoint (withCallbackContext ctx outer)) + +@[simp] theorem withCallbackContext_sender (ctx : CallbackContext) + (world : Verity.ContractState) : + (withCallbackContext ctx world).sender = ctx.sender := rfl + +@[simp] theorem withCallbackContext_msgValue (ctx : CallbackContext) + (world : Verity.ContractState) : + (withCallbackContext ctx world).msgValue = ctx.msgValue := rfl + +@[simp] theorem withCallbackContext_calldata (ctx : CallbackContext) + (world : Verity.ContractState) : + (withCallbackContext ctx world).calldata = ctx.calldata := rfl + +@[simp] theorem withCallbackContext_calldataSize (ctx : CallbackContext) + (world : Verity.ContractState) : + (withCallbackContext ctx world).calldataSize = ctx.calldataSize := rfl + +@[simp] theorem callbackTransition_restores_sender (ctx : CallbackContext) + (entrypoint : Verity.ContractState → Verity.ContractState) + (outer : Verity.ContractState) : + (callbackTransition ctx entrypoint outer).sender = outer.sender := rfl + +@[simp] theorem callbackTransition_restores_msgValue (ctx : CallbackContext) + (entrypoint : Verity.ContractState → Verity.ContractState) + (outer : Verity.ContractState) : + (callbackTransition ctx entrypoint outer).msgValue = outer.msgValue := rfl + +@[simp] theorem callbackTransition_restores_calldata (ctx : CallbackContext) + (entrypoint : Verity.ContractState → Verity.ContractState) + (outer : Verity.ContractState) : + (callbackTransition ctx entrypoint outer).calldata = outer.calldata := rfl + +@[simp] theorem callbackTransition_restores_calldataSize (ctx : CallbackContext) + (entrypoint : Verity.ContractState → Verity.ContractState) + (outer : Verity.ContractState) : + (callbackTransition ctx entrypoint outer).calldataSize = outer.calldataSize := rfl + /-- Each mutable transition is some finite reentry schedule drawn from the registry. Static sites are unrestricted: `denoteCall` never commits their transitions, and `Conforms` separately pins them externally. -/ diff --git a/Verity/Core/Model/NonReentrantGuard.lean b/Verity/Core/Model/NonReentrantGuard.lean index 0ca472b68..5e1aff6b6 100644 --- a/Verity/Core/Model/NonReentrantGuard.lean +++ b/Verity/Core/Model/NonReentrantGuard.lean @@ -47,23 +47,24 @@ def setLock (slot : Nat) (value : Uint256) (s : ContractState) : ContractState : /-- Executable semantics of a `nonreentrant(slot)` entrypoint. -/ def guarded (slot : Nat) (body : Contract α) : Contract α := fun s => - if s.transientStorage slot = 0 then + if s.transientStorage slot ≠ 1 then match body.run (setLock slot 1 s) with | .success a s' => .success a (setLock slot 0 s') | .revert msg _ => .revert msg s else ContractResult.revert "reentrant call blocked" s -/-- Lock held → the guarded entrypoint reverts without touching the state. -/ +/-- The compiler's sentinel value is held → the guarded entrypoint reverts +without touching the state. Other transient values are not treated as held. -/ theorem guarded_locked_reverts (slot : Nat) (body : Contract α) - (s : ContractState) (hlock : s.transientStorage slot ≠ 0) : + (s : ContractState) (hlock : s.transientStorage slot = 1) : guarded slot body s = ContractResult.revert "reentrant call blocked" s := by simp [guarded, hlock] /-- Lock free → the body runs from the locked state; successful exits release the lock, reverting exits roll back to the pre-call state. -/ theorem guarded_free_runs_body (slot : Nat) (body : Contract α) - (s : ContractState) (hfree : s.transientStorage slot = 0) : + (s : ContractState) (hfree : s.transientStorage slot ≠ 1) : guarded slot body s = match body.run (setLock slot 1 s) with | .success a s' => .success a (setLock slot 0 s') @@ -81,18 +82,18 @@ theorem guarded_success_releases (slot : Nat) (body : Contract α) (s s' : ContractState) (a : α) (hrun : guarded slot body s = ContractResult.success a s') : s'.transientStorage slot = 0 := by - by_cases hfree : s.transientStorage slot = 0 + by_cases hfree : s.transientStorage slot ≠ 1 · rw [guarded_free_runs_body slot body s hfree] at hrun cases hbody : body.run (setLock slot 1 s) with | success b sb => rw [hbody] at hrun; injection hrun with _ hs; rw [← hs]; simp | revert msg sr => rw [hbody] at hrun; cases hrun - · rw [guarded_locked_reverts slot body s hfree] at hrun + · rw [guarded_locked_reverts slot body s (by simpa using hfree)] at hrun cases hrun /-- The reentry-window theorem: while the lock is held, a callback into any same-lock guarded entrypoint is the identity as a state transformer. -/ theorem guarded_reentry_blocked (slot : Nat) (body : Contract α) - (s : ContractState) (hlock : s.transientStorage slot ≠ 0) : + (s : ContractState) (hlock : s.transientStorage slot = 1) : (guarded slot body).runState s = s := by unfold Contract.runState rw [guarded_locked_reverts slot body s hlock] @@ -101,7 +102,7 @@ theorem guarded_reentry_blocked (slot : Nat) (body : Contract α) identity on locked states: no interleaving of guarded entrypoints can act inside the window. This is the schedule-level closure of the guard. -/ theorem runSeq_guarded_locked_id (slot : Nat) (entries : List (Contract Unit)) - (s : ContractState) (hlock : s.transientStorage slot ≠ 0) : + (s : ContractState) (hlock : s.transientStorage slot = 1) : runSeq (entries.map (fun entry => (guarded slot entry).runState)) s = s := by induction entries with | nil => rfl @@ -114,7 +115,7 @@ theorem runSeq_guarded_locked_id (slot : Nat) (entries : List (Contract Unit)) packaged in the `Preserves` shape used by `ReentrancySpec` registries. -/ theorem guarded_preserves_on_locked (slot : Nat) (body : Contract Unit) (Inv : ContractState → Prop) : - ∀ s, s.transientStorage slot ≠ 0 → Inv s → + ∀ s, s.transientStorage slot = 1 → Inv s → Inv ((guarded slot body).runState s) := by intro s hlock hInv rw [guarded_reentry_blocked slot body s hlock] diff --git a/Verity/Macro/Translate.lean b/Verity/Macro/Translate.lean index 7c2b6080b..81dfa8c78 100644 --- a/Verity/Macro/Translate.lean +++ b/Verity/Macro/Translate.lean @@ -2265,6 +2265,19 @@ def translatedBodyOpensReentrancyWindow | _ => throwErrorAt bodyTerm "failed to reduce the translated reentrancy-window predicate" +def translatedBodyContainsExternalCall + (stmtTerms : Array Term) : CommandElabM Bool := do + let bodyTerm : Term ← `([ $[$stmtTerms],* ]) + liftTermElabM do + let predicate : Term ← + `($(bodyTerm).any Compiler.CompilationModel.stmtContainsExternalCall) + let expr ← Lean.Elab.Term.elabTermEnsuringType predicate (mkConst ``Bool) + match ← Lean.Meta.withTransparency .all (Lean.Meta.whnf expr) with + | .const ``Bool.true _ => pure true + | .const ``Bool.false _ => pure false + | _ => throwErrorAt bodyTerm + "failed to reduce the translated external-call predicate" + private partial def syntaxCallsAnyHelper (helperNames : Array String) (stx : Syntax) : CommandElabM Bool := do match stx with @@ -2405,6 +2418,7 @@ private def threadHelperApp? (constDecls : Array ConstantDecl) (immutableDecls : Array ImmutableDecl) (externalDecls : Array ExternalDecl) (helpers : Array FunctionDecl) (adversarialHelpers : Array FunctionDecl) + (registryOnlyHelpers : Array FunctionDecl) (params : Array ParamDecl) (locals : Array TypedLocal) (name : Ident) (args : Array Term) (adv : Term) : CommandElabM (Option Term) := do @@ -2436,8 +2450,14 @@ private def threadHelperApp? -- is not a local helper call. Leave it to the recursive traversal so -- linked calls nested in its arguments still receive the adversary. return none + let registryOnly := registryOnlyHelpers.any (fun candidate => + functionSignatureKey candidate == functionSignatureKey helper) let target ← - if helper.nonReentrantLock.isSome && helper.reentrancyTrusted && + if registryOnly && helper.nonReentrantLock.isSome && helper.reentrancyTrusted then + mkSuffixedIdent helper.ident "_registry_unguarded" + else if registryOnly then + mkSuffixedIdent helper.ident "_registry" + else if helper.nonReentrantLock.isSome && helper.reentrancyTrusted && matchesExactHelper helper then mkSuffixedIdent helper.ident "_unguarded" else @@ -2767,11 +2787,12 @@ private partial def threadAdversaryThroughExecutableSyntax (externalDecls : Array ExternalDecl) (helpers : Array FunctionDecl) (adversarialHelpers : Array FunctionDecl) + (registryOnlyHelpers : Array FunctionDecl) (params : Array ParamDecl) (locals : Array TypedLocal) (adv : Term) (stx : Syntax) : CommandElabM Syntax := do let go := threadAdversaryThroughExecutableSyntax fields constDecls immutableDecls - externalDecls helpers adversarialHelpers params locals adv + externalDecls helpers adversarialHelpers registryOnlyHelpers params locals adv let recurseChildren : CommandElabM Syntax := do match stx with | .node info kind args => @@ -2792,30 +2813,36 @@ private partial def threadAdversaryThroughExecutableSyntax | none => inferPureExprType fields constDecls immutableDecls externalDecls params scope rhs pure (scope.push (mkTypedLocal (toString name.getId) ty)) catch _ => pure scope - let inferTuple (names : Array (Option String)) (rhs : Term) := do + let inferTuple (origin : Syntax) (names : Array (Option String)) (rhs : Term) := do try - match ← inferTupleSourceTypes? fields constDecls immutableDecls externalDecls - helpers params scope rhs with - | some valueTys => - if names.size != valueTys.size then - pure scope - else - let typedNames := (names.zip valueTys).filterMap fun (name?, ty) => - name?.map (fun name => mkTypedLocal name ty) - pure (scope ++ typedNames) - | none => pure scope + match ← resolveQualifiedFunctionApp? fields constDecls immutableDecls externalDecls + params scope rhs with + | some (qualifiedName, _) => + let typedNames ← unsafe qualifiedTupleBindTypedLocals origin qualifiedName names + pure (scope ++ typedNames) + | none => + match ← inferTupleSourceTypes? fields constDecls immutableDecls externalDecls + helpers params scope rhs with + | some valueTys => + if names.size != valueTys.size then + pure scope + else + let typedNames := (names.zip valueTys).filterMap fun (name?, ty) => + name?.map (fun name => mkTypedLocal name ty) + pure (scope ++ typedNames) + | none => pure scope catch _ => pure scope let tupleScope? ← do let stx := elem.raw if stx.getKind == `Lean.Parser.Term.doLet then let patDecl := stx[3][0] match tupleBinderNames? patDecl[0] with - | some names => pure (some (← inferTuple names ⟨patDecl[4]⟩)) + | some names => pure (some (← inferTuple patDecl names ⟨patDecl[4]⟩)) | none => pure none else if stx.getKind == `Lean.Parser.Term.doLetArrow then let patDecl := stx[3] match tupleBinderNames? patDecl[0] with - | some names => pure (some (← inferTuple names ⟨patDecl[3][0]⟩)) + | some names => pure (some (← inferTuple patDecl names ⟨patDecl[3][0]⟩)) | none => pure none else pure none @@ -2858,16 +2885,16 @@ private partial def threadAdversaryThroughExecutableSyntax let rewrittenBody : TSyntax ``Lean.Parser.Term.doSeq := ⟨bodyRaw⟩ pure (#[], ← `(term| do $rewrittenBody)) | `(term| $name:ident($[$args:term],*)) => + let mut binds : Array (Ident × Term) := #[] + let mut rewrittenArgs : Array Term := #[] + for arg in args do + let (inner, rewritten) ← hoistNested true arg + binds := binds ++ inner + rewrittenArgs := rewrittenArgs.push rewritten match ← threadHelperApp? fields constDecls immutableDecls externalDecls - helpers adversarialHelpers params locals name args adv with - | some app => pure (#[], app) + helpers adversarialHelpers registryOnlyHelpers params locals name rewrittenArgs adv with + | some app => pure (binds, app) | none => - let mut binds : Array (Ident × Term) := #[] - let mut rewrittenArgs : Array Term := #[] - for arg in args do - let (inner, rewritten) ← hoistNested true arg - binds := binds ++ inner - rewrittenArgs := rewrittenArgs.push rewritten let mut app : Term := ⟨name.raw⟩ for arg in rewrittenArgs do app ← `(term| $app $arg) @@ -2951,7 +2978,7 @@ private partial def threadAdversaryThroughExecutableSyntax let mut rewritten : Array (TSyntax `doElem) := #[] for elem in elems do let raw ← threadAdversaryThroughExecutableSyntax fields constDecls immutableDecls - externalDecls helpers adversarialHelpers params scope adv elem.raw + externalDecls helpers adversarialHelpers registryOnlyHelpers params scope adv elem.raw rewritten := rewritten.push ⟨raw⟩ scope ← extendLocals scope elem `(doSeq| $[$rewritten:doElem]*) @@ -3006,7 +3033,7 @@ private partial def threadAdversaryThroughExecutableSyntax (externalArgAddress $rewrittenToken) $adv))) | `(doElem| let $name:ident ← $fn:ident($[$args:term],*)) => match ← threadHelperApp? fields constDecls immutableDecls externalDecls - helpers adversarialHelpers params locals fn args adv with + helpers adversarialHelpers registryOnlyHelpers params locals fn args adv with | some app => `(doElem| let $name ← $app:term) | none => recurseChildren | `(doElem| let $name:ident ← $fn:ident $args:term*) => @@ -3032,7 +3059,7 @@ private partial def threadAdversaryThroughExecutableSyntax (← `(doElem| let $name ← (totalSupply (externalArgAddress $token) $adv))) else match ← threadHelperApp? fields constDecls immutableDecls externalDecls - helpers adversarialHelpers params locals fn original adv with + helpers adversarialHelpers registryOnlyHelpers params locals fn original adv with | some app => hoistLive false app fun rewritten => `(doElem| let $name ← $rewritten:term) | none => @@ -3161,7 +3188,7 @@ private partial def threadAdversaryThroughExecutableSyntax | _ => recurseChildren else match ← threadHelperApp? fields constDecls immutableDecls externalDecls - helpers adversarialHelpers params locals fn original adv with + helpers adversarialHelpers registryOnlyHelpers params locals fn original adv with | some app => hoistLive false app fun rewritten => `(doElem| $rewritten:term) | none => @@ -3174,7 +3201,7 @@ private partial def threadAdversaryThroughExecutableSyntax | `(term| $name:ident $args:term*) => let original := args.map fun arg => (⟨arg.raw⟩ : Term) match ← threadHelperApp? fields constDecls immutableDecls externalDecls - helpers adversarialHelpers params locals name original adv with + helpers adversarialHelpers registryOnlyHelpers params locals name original adv with | some app => pure app.raw | none => if isLiveStateExternalCall ⟨stx⟩ then @@ -5164,6 +5191,7 @@ def validateGeneratedDefNamesPublic let helperNames := #[ s!"{generatedFnName}_modelBody" , s!"{generatedFnName}_entrypoint" + , s!"{generatedFnName}_registry" , s!"{generatedFnName}_model" , s!"{generatedFnName}_bridge" , s!"{generatedFnName}_semantic_preservation" @@ -5181,7 +5209,8 @@ def validateGeneratedDefNamesPublic ] let helperNames := if fn.nonReentrantLock.isSome && fn.reentrancyTrusted then - helperNames.push s!"{generatedFnName}_unguarded" + (helperNames.push s!"{generatedFnName}_unguarded").push + s!"{generatedFnName}_registry_unguarded" else helperNames for helperName in helperNames do @@ -5461,7 +5490,7 @@ def mkConstructorDefCommandPublic else `(Compiler.CompilationModel.DenoteExternalCalls.AdversaryModel.stub) let executableBody := ⟨← threadAdversaryThroughExecutableSyntax fields constDecls immutableDecls - externalDecls functions adversarialHelpers ctor.params #[] advTerm executableBody.raw⟩ + externalDecls functions adversarialHelpers #[] ctor.params #[] advTerm executableBody.raw⟩ let fnType ← if opensReentrancyWindow then mkContractFnTypeWithAdversary ctor.params .unit else @@ -5531,7 +5560,7 @@ def mkHostConstructorDefCommandPublic let body ← `(term| do $[$preludes:doElem]* $[$elems:doElem]*) let executableBody ← rewriteForEachExecutableBody fields externalDecls ctor.params body let executableBody := ⟨← threadAdversaryThroughExecutableSyntax fields constDecls immutableDecls - externalDecls functions ownAdversarialHelpers ctor.params #[] advTerm executableBody.raw⟩ + externalDecls functions ownAdversarialHelpers #[] ctor.params #[] advTerm executableBody.raw⟩ let fnValue ← if containsExternalCall then mkContractFnValueWithAdversary advIdent ctor.params executableBody else @@ -5560,10 +5589,17 @@ def mkIncludeAliasCommandsPublic let predicateId ← mkSuffixedIdent fn.ident "_entrypoint" let predicateTgt ← mkSuffixedIdent tgt "_entrypoint" cmds := cmds.push (← `(command| abbrev $predicateId := $predicateTgt)) + let registryId ← mkSuffixedIdent fn.ident "_registry" + let registryTgt ← mkSuffixedIdent tgt "_registry" + cmds := cmds.push (← `(command| abbrev $registryId := $registryTgt)) if fn.nonReentrantLock.isSome && fn.reentrancyTrusted then let unguardedId ← mkSuffixedIdent fn.ident "_unguarded" let unguardedTgt ← mkSuffixedIdent tgt "_unguarded" cmds := cmds.push (← `(command| abbrev $unguardedId := $unguardedTgt)) + let registryUnguardedId ← mkSuffixedIdent fn.ident "_registry_unguarded" + let registryUnguardedTgt ← mkSuffixedIdent tgt "_registry_unguarded" + cmds := cmds.push + (← `(command| abbrev $registryUnguardedId := $registryUnguardedTgt)) for modDecl in mixin.modifiers do unless modifierContainsExternalCallSyntaxPublic modDecl do let tgt := mkIdent (mixinName ++ modDecl.ident.getId) @@ -5635,8 +5671,13 @@ def mkFunctionCommandsPublic | some inlined => pure inlined | none => pure fn let stmtTerms ← translateBodyToStmtTerms fields roleDecls errorDecls constDecls immutableDecls externalDecls functions modelFn + -- Executable registry transitions must use the explicit adversary for every + -- external-call-dependent result, including static calls whose returndata + -- can influence a later storage write. Reentrancy-window classification is + -- intentionally narrower and is therefore not sufficient for this path. let directlyOpensReentrancyWindow ← translatedBodyOpensReentrancyWindow stmtTerms let mut adversarialHelpers : Array FunctionDecl := #[] + let mut windowHelpers : Array FunctionDecl := #[] let mut translatedHelpers : Array (FunctionDecl × FunctionDecl) := #[] for helper in functions do let helperModel ← @@ -5650,8 +5691,10 @@ def mkFunctionCommandsPublic let helperStmtTerms ← translateBodyToStmtTerms fields roleDecls errorDecls constDecls immutableDecls externalDecls functions helperModel translatedHelpers := translatedHelpers.push (helper, helperModel) - if ← translatedBodyOpensReentrancyWindow helperStmtTerms then + if ← translatedBodyContainsExternalCall helperStmtTerms then adversarialHelpers := adversarialHelpers.push helper + if ← translatedBodyOpensReentrancyWindow helperStmtTerms then + windowHelpers := windowHelpers.push helper -- Reentrancy-window capability is transitive across internal helpers. Iterate to a -- fixed point so every caller in a multi-hop helper chain receives and forwards -- the same adversary instead of silently falling back to the stub. @@ -5666,10 +5709,23 @@ def mkFunctionCommandsPublic grew := true if !grew then break - let adversarialNames := adversarialHelpers.map (·.name) - let callsAdversarial ← syntaxCallsAnyHelper adversarialNames modelFn.body.raw - let opensReentrancyWindow := directlyOpensReentrancyWindow || - callsAdversarial + for _ in [:functions.size] do + let windowNames := windowHelpers.map (·.name) + let mut grew := false + for (helper, helperModel) in translatedHelpers do + let callsWindow ← syntaxCallsAnyHelper windowNames helperModel.body.raw + if !windowHelpers.any (fun candidate => candidate.name == helper.name) && + callsWindow then + windowHelpers := windowHelpers.push helper + grew := true + if !grew then + break + let windowNames := windowHelpers.map (·.name) + let callsWindow ← syntaxCallsAnyHelper windowNames modelFn.body.raw + let opensReentrancyWindow := directlyOpensReentrancyWindow || callsWindow + let registryOnlyHelpers := adversarialHelpers.filter fun helper => + !windowHelpers.any (fun candidate => + functionSignatureKey candidate == functionSignatureKey helper) -- Keep the generated binder hygienic: source parameters and locals are allowed -- to use `_adv` without capturing the adversary threaded into rewritten calls. let advIdent ← Lean.Elab.Term.mkFreshIdent (mkIdentFrom fn.ident `_adv).raw @@ -5681,28 +5737,51 @@ def mkFunctionCommandsPublic mkContractFnTypeWithAdversary fn.params fn.returnTy else mkContractFnType fn.params fn.returnTy - let fnExecutableBody := ⟨← threadAdversaryThroughExecutableSyntax fields constDecls immutableDecls - externalDecls functions adversarialHelpers fn.params #[] advTerm fnExecutableBody.raw⟩ + let publicExecutableBody := ⟨← threadAdversaryThroughExecutableSyntax fields constDecls immutableDecls + externalDecls functions windowHelpers #[] fn.params #[] advTerm fnExecutableBody.raw⟩ + let registryExecutableBody := ⟨← threadAdversaryThroughExecutableSyntax fields constDecls immutableDecls + externalDecls functions adversarialHelpers registryOnlyHelpers fn.params #[] + (⟨advIdent.raw⟩ : Term) fnExecutableBody.raw⟩ let mut extraExecutableCmds : Array Cmd := #[] if fn.nonReentrantLock.isSome && fn.reentrancyTrusted then let unguardedId ← mkSuffixedIdent fn.ident "_unguarded" let unguardedValue ← if opensReentrancyWindow then - mkContractFnValueWithAdversary advIdent fn.params fnExecutableBody + mkContractFnValueWithAdversary advIdent fn.params publicExecutableBody else - mkContractFnValue fn.params fnExecutableBody + mkContractFnValue fn.params publicExecutableBody extraExecutableCmds := extraExecutableCmds.push (← `(command| def $unguardedId : $fnType := $unguardedValue)) - let fnExecutableBody ← match fn.nonReentrantLock with + let publicExecutableBody ← match fn.nonReentrantLock with | some lockIdent => let lockName := toString lockIdent.getId let some lockField := fields.find? (fun field => field.name == lockName) | throwErrorAt lockIdent s!"unknown nonreentrant lock field '{lockName}'" - `(Verity.Core.NonReentrantGuard.guarded $(natTerm lockField.slotNum) $fnExecutableBody) - | none => pure fnExecutableBody + `(Verity.Core.NonReentrantGuard.guarded $(natTerm lockField.slotNum) $publicExecutableBody) + | none => pure publicExecutableBody let fnValue ← if opensReentrancyWindow then - mkContractFnValueWithAdversary advIdent fn.params fnExecutableBody + mkContractFnValueWithAdversary advIdent fn.params publicExecutableBody else - mkContractFnValue fn.params fnExecutableBody + mkContractFnValue fn.params publicExecutableBody + let registryId ← mkSuffixedIdent fn.ident "_registry" + let registryType ← mkContractFnTypeWithAdversary fn.params fn.returnTy + if fn.nonReentrantLock.isSome && fn.reentrancyTrusted then + let registryUnguardedId ← mkSuffixedIdent fn.ident "_registry_unguarded" + let registryUnguardedValue ← + mkContractFnValueWithAdversary advIdent fn.params registryExecutableBody + extraExecutableCmds := extraExecutableCmds.push + (← `(command| def $registryUnguardedId : $registryType := $registryUnguardedValue)) + let registryGuardedBody ← match fn.nonReentrantLock with + | some lockIdent => + let lockName := toString lockIdent.getId + let some lockField := fields.find? (fun field => field.name == lockName) + | throwErrorAt lockIdent s!"unknown nonreentrant lock field '{lockName}'" + `(Verity.Core.NonReentrantGuard.guarded + $(natTerm lockField.slotNum) $registryExecutableBody) + | none => pure registryExecutableBody + let registryValue ← + mkContractFnValueWithAdversary advIdent fn.params registryGuardedBody + extraExecutableCmds := extraExecutableCmds.push + (← `(command| def $registryId : $registryType := $registryValue)) let modelParams ← mkModelParamsTerm fn.params let localObligationTerms ← (functionLocalObligationsWithArithmetic fn).mapM mkModelLocalObligationTerm let payableTerm ← if fn.isPayable then `(true) else `(false) @@ -5728,11 +5807,13 @@ def mkFunctionCommandsPublic (mkIdentFrom fn.ident `_registryAdv).raw let transitionIdent ← Lean.Elab.Term.mkFreshIdent (mkIdentFrom fn.ident `_transition).raw + let contextIdent ← Lean.Elab.Term.mkFreshIdent + (mkIdentFrom fn.ident `_callbackContext).raw let registryAdv : Ident := ⟨registryAdvIdent.raw⟩ let transition : Ident := ⟨transitionIdent.raw⟩ - let mut applied : Term := fn.ident - if opensReentrancyWindow then - applied ← `($applied (ExecutableCallContext.ofAdversary $registryAdv:ident)) + let context : Ident := ⟨contextIdent.raw⟩ + let mut applied : Term := registryId + applied ← `($applied (ExecutableCallContext.ofAdversary $registryAdv:ident)) let mut registryParams : Array (Ident × Term) := #[] for param in fn.params do let paramTy ← contractValueTypeTerm param.ty @@ -5743,9 +5824,14 @@ def mkFunctionCommandsPublic applied ← `($applied $registryParam:ident) let mut registryBody : Term ← `(($transition:ident : Verity.ContractState → Verity.ContractState) = - ($applied).runState) + Compiler.CompilationModel.DenoteExternalCalls.callbackTransition + $context:ident ($applied).runState) for (paramIdent, paramTy) in registryParams.reverse do registryBody ← `(∃ $paramIdent:ident : $paramTy, $registryBody) + registryBody ← + `(∃ $context:ident : + Compiler.CompilationModel.DenoteExternalCalls.CallbackContext, + $registryBody) let entrypointCmd : Cmd ← `(command| def $entrypointPredicateName ($registryAdv:ident : diff --git a/Verity/Proofs/Model/GeneratedEntrypointRegistry.lean b/Verity/Proofs/Model/GeneratedEntrypointRegistry.lean index 29df6ddaf..0a8d0a449 100644 --- a/Verity/Proofs/Model/GeneratedEntrypointRegistry.lean +++ b/Verity/Proofs/Model/GeneratedEntrypointRegistry.lean @@ -30,16 +30,18 @@ open Compiler.CompilationModel.DenoteExternalCalls /-- The generated registry uses its explicit adversary at the external-call entrypoint; there is no `.stub` compatibility path in this theorem surface. -/ -theorem guardedPing_registered (adv : AdversaryModel) (value : Uint256) : +theorem guardedPing_registered (adv : AdversaryModel) (ctx : CallbackContext) + (value : Uint256) : entrypointRegistry adv - (guardedPing (ExecutableCallContext.ofAdversary adv) value).runState := by + (callbackTransition ctx + (guardedPing_registry (ExecutableCallContext.ofAdversary adv) value).runState) := by left - exact ⟨value, rfl⟩ + exact ⟨ctx, value, rfl⟩ /-- The executable generated entrypoint is definitionally protected by the canonical source guard at the same slot used by the compiled dispatch guard. -/ theorem guardedPing_reentry_blocked (adv : AdversaryModel) (value : Uint256) - (state : ContractState) (hlock : state.transientStorage 0 ≠ 0) : + (state : ContractState) (hlock : state.transientStorage 0 = 1) : (guardedPing (ExecutableCallContext.ofAdversary adv) value).runState state = state := by apply Verity.Core.NonReentrantGuard.guarded_reentry_blocked exact hlock From 9cf62cb5846e830c4444e0d6a044ec555a915c45 Mon Sep 17 00:00:00 2001 From: Thomas Marchand Date: Tue, 8 Sep 2026 08:47:42 +0200 Subject: [PATCH 24/50] test(reentrancy): refresh registry macro fixture --- ...onreentrantQualifiedHelperResolution.t.sol | 31 ++++++++++++++----- 1 file changed, 23 insertions(+), 8 deletions(-) diff --git a/artifacts/macro_property_tests/PropertyNonreentrantQualifiedHelperResolution.t.sol b/artifacts/macro_property_tests/PropertyNonreentrantQualifiedHelperResolution.t.sol index 94cd69b9d..9a290db10 100644 --- a/artifacts/macro_property_tests/PropertyNonreentrantQualifiedHelperResolution.t.sol +++ b/artifacts/macro_property_tests/PropertyNonreentrantQualifiedHelperResolution.t.sol @@ -145,49 +145,64 @@ contract PropertyNonreentrantQualifiedHelperResolutionTest is YulTestBase { // TODO(#1011): decode `ret` and assert the concrete postcondition from Lean theorem. ret; } - // Property 15: overloadedTrustedCaller has no unexpected revert + // Property 15: TODO decode and assert `trustedNestedExternal` result + function testTODO_TrustedNestedExternal_DecodeAndAssert() public { + vm.prank(alice); + (bool ok, bytes memory ret) = target.call(abi.encodeWithSignature("trustedNestedExternal(uint256)", uint256(1))); + require(ok, "trustedNestedExternal reverted unexpectedly"); + assertEq(ret.length, 32, "trustedNestedExternal ABI return length mismatch (expected 32 bytes)"); + // TODO(#1011): decode `ret` and assert the concrete postcondition from Lean theorem. + ret; + } + // Property 16: overloadedTrustedCaller has no unexpected revert function testAuto_OverloadedTrustedCaller_NoUnexpectedRevert() public { vm.prank(alice); (bool ok,) = target.call(abi.encodeWithSignature("overloadedTrustedCaller(uint256)", uint256(1))); require(ok, "overloadedTrustedCaller reverted unexpectedly"); } - // Property 16: overloadedAdversarialCaller has no unexpected revert + // Property 17: overloadedAdversarialCaller has no unexpected revert function testAuto_OverloadedAdversarialCaller_NoUnexpectedRevert() public { vm.prank(alice); (bool ok,) = target.call(abi.encodeWithSignature("overloadedAdversarialCaller(uint256)", uint256(1))); require(ok, "overloadedAdversarialCaller reverted unexpectedly"); } - // Property 17: overloadedTrustedLocalCaller has no unexpected revert + // Property 18: overloadedTrustedLocalCaller has no unexpected revert function testAuto_OverloadedTrustedLocalCaller_NoUnexpectedRevert() public { vm.prank(alice); (bool ok,) = target.call(abi.encodeWithSignature("overloadedTrustedLocalCaller()")); require(ok, "overloadedTrustedLocalCaller reverted unexpectedly"); } - // Property 18: overloadedAdversarialLocalCaller has no unexpected revert + // Property 19: overloadedAdversarialLocalCaller has no unexpected revert function testAuto_OverloadedAdversarialLocalCaller_NoUnexpectedRevert() public { vm.prank(alice); (bool ok,) = target.call(abi.encodeWithSignature("overloadedAdversarialLocalCaller()")); require(ok, "overloadedAdversarialLocalCaller reverted unexpectedly"); } - // Property 19: overloadedTrustedTupleLocalCaller has no unexpected revert + // Property 20: overloadedTrustedTupleLocalCaller has no unexpected revert function testAuto_OverloadedTrustedTupleLocalCaller_NoUnexpectedRevert() public { vm.prank(alice); (bool ok,) = target.call(abi.encodeWithSignature("overloadedTrustedTupleLocalCaller(uint256)", uint256(1))); require(ok, "overloadedTrustedTupleLocalCaller reverted unexpectedly"); } - // Property 20: overloadedAdversarialTupleLocalCaller has no unexpected revert + // Property 21: overloadedAdversarialTupleLocalCaller has no unexpected revert function testAuto_OverloadedAdversarialTupleLocalCaller_NoUnexpectedRevert() public { vm.prank(alice); (bool ok,) = target.call(abi.encodeWithSignature("overloadedAdversarialTupleLocalCaller(uint256)", uint256(1))); require(ok, "overloadedAdversarialTupleLocalCaller reverted unexpectedly"); } - // Property 21: overloadedTrustedForEachCaller has no unexpected revert + // Property 22: overloadedTrustedQualifiedTupleCaller has no unexpected revert + function testAuto_OverloadedTrustedQualifiedTupleCaller_NoUnexpectedRevert() public { + vm.prank(alice); + (bool ok,) = target.call(abi.encodeWithSignature("overloadedTrustedQualifiedTupleCaller(uint256)", uint256(1))); + require(ok, "overloadedTrustedQualifiedTupleCaller reverted unexpectedly"); + } + // Property 23: overloadedTrustedForEachCaller has no unexpected revert function testAuto_OverloadedTrustedForEachCaller_NoUnexpectedRevert() public { vm.prank(alice); (bool ok,) = target.call(abi.encodeWithSignature("overloadedTrustedForEachCaller()")); require(ok, "overloadedTrustedForEachCaller reverted unexpectedly"); } - // Property 22: overloadedAdversarialForEachSetBitCaller has no unexpected revert + // Property 24: overloadedAdversarialForEachSetBitCaller has no unexpected revert function testAuto_OverloadedAdversarialForEachSetBitCaller_NoUnexpectedRevert() public { vm.prank(alice); (bool ok,) = target.call(abi.encodeWithSignature("overloadedAdversarialForEachSetBitCaller()")); From 01b17a9f34b3beb7e7af391ce279a04bb30a6086 Mon Sep 17 00:00:00 2001 From: Thomas Marchand Date: Tue, 8 Sep 2026 10:39:28 +0200 Subject: [PATCH 25/50] fix(reentrancy): align callback frame and guard --- Verity/Core/Model/CallbackBridge.lean | 12 +++++++++++- Verity/Core/Model/NonReentrantGuard.lean | 19 +++++++++---------- .../Model/GeneratedEntrypointRegistry.lean | 2 +- 3 files changed, 21 insertions(+), 12 deletions(-) diff --git a/Verity/Core/Model/CallbackBridge.lean b/Verity/Core/Model/CallbackBridge.lean index e39c305af..8c9063d2a 100644 --- a/Verity/Core/Model/CallbackBridge.lean +++ b/Verity/Core/Model/CallbackBridge.lean @@ -60,8 +60,10 @@ def withCallbackContext (ctx : CallbackContext) (world : Verity.ContractState) : { world with sender := ctx.sender msgValue := ctx.msgValue + selfBalance := world.selfBalance + ctx.msgValue calldataSize := ctx.calldataSize - calldata := ctx.calldata } + calldata := ctx.calldata + returndata := [] } def restoreCallbackContext (outer callbackResult : Verity.ContractState) : Verity.ContractState := @@ -92,6 +94,14 @@ def callbackTransition (ctx : CallbackContext) (world : Verity.ContractState) : (withCallbackContext ctx world).calldataSize = ctx.calldataSize := rfl +@[simp] theorem withCallbackContext_selfBalance (ctx : CallbackContext) + (world : Verity.ContractState) : + (withCallbackContext ctx world).selfBalance = world.selfBalance + ctx.msgValue := rfl + +@[simp] theorem withCallbackContext_returndata (ctx : CallbackContext) + (world : Verity.ContractState) : + (withCallbackContext ctx world).returndata = [] := rfl + @[simp] theorem callbackTransition_restores_sender (ctx : CallbackContext) (entrypoint : Verity.ContractState → Verity.ContractState) (outer : Verity.ContractState) : diff --git a/Verity/Core/Model/NonReentrantGuard.lean b/Verity/Core/Model/NonReentrantGuard.lean index 5e1aff6b6..86ffc25f4 100644 --- a/Verity/Core/Model/NonReentrantGuard.lean +++ b/Verity/Core/Model/NonReentrantGuard.lean @@ -47,24 +47,23 @@ def setLock (slot : Nat) (value : Uint256) (s : ContractState) : ContractState : /-- Executable semantics of a `nonreentrant(slot)` entrypoint. -/ def guarded (slot : Nat) (body : Contract α) : Contract α := fun s => - if s.transientStorage slot ≠ 1 then + if s.transientStorage slot = 0 then match body.run (setLock slot 1 s) with | .success a s' => .success a (setLock slot 0 s') | .revert msg _ => .revert msg s else ContractResult.revert "reentrant call blocked" s -/-- The compiler's sentinel value is held → the guarded entrypoint reverts -without touching the state. Other transient values are not treated as held. -/ +/-- Any nonzero lock value is held, matching the compiled `tload` guard. -/ theorem guarded_locked_reverts (slot : Nat) (body : Contract α) - (s : ContractState) (hlock : s.transientStorage slot = 1) : + (s : ContractState) (hlock : s.transientStorage slot ≠ 0) : guarded slot body s = ContractResult.revert "reentrant call blocked" s := by simp [guarded, hlock] /-- Lock free → the body runs from the locked state; successful exits release the lock, reverting exits roll back to the pre-call state. -/ theorem guarded_free_runs_body (slot : Nat) (body : Contract α) - (s : ContractState) (hfree : s.transientStorage slot ≠ 1) : + (s : ContractState) (hfree : s.transientStorage slot = 0) : guarded slot body s = match body.run (setLock slot 1 s) with | .success a s' => .success a (setLock slot 0 s') @@ -82,18 +81,18 @@ theorem guarded_success_releases (slot : Nat) (body : Contract α) (s s' : ContractState) (a : α) (hrun : guarded slot body s = ContractResult.success a s') : s'.transientStorage slot = 0 := by - by_cases hfree : s.transientStorage slot ≠ 1 + by_cases hfree : s.transientStorage slot = 0 · rw [guarded_free_runs_body slot body s hfree] at hrun cases hbody : body.run (setLock slot 1 s) with | success b sb => rw [hbody] at hrun; injection hrun with _ hs; rw [← hs]; simp | revert msg sr => rw [hbody] at hrun; cases hrun - · rw [guarded_locked_reverts slot body s (by simpa using hfree)] at hrun + · rw [guarded_locked_reverts slot body s hfree] at hrun cases hrun /-- The reentry-window theorem: while the lock is held, a callback into any same-lock guarded entrypoint is the identity as a state transformer. -/ theorem guarded_reentry_blocked (slot : Nat) (body : Contract α) - (s : ContractState) (hlock : s.transientStorage slot = 1) : + (s : ContractState) (hlock : s.transientStorage slot ≠ 0) : (guarded slot body).runState s = s := by unfold Contract.runState rw [guarded_locked_reverts slot body s hlock] @@ -102,7 +101,7 @@ theorem guarded_reentry_blocked (slot : Nat) (body : Contract α) identity on locked states: no interleaving of guarded entrypoints can act inside the window. This is the schedule-level closure of the guard. -/ theorem runSeq_guarded_locked_id (slot : Nat) (entries : List (Contract Unit)) - (s : ContractState) (hlock : s.transientStorage slot = 1) : + (s : ContractState) (hlock : s.transientStorage slot ≠ 0) : runSeq (entries.map (fun entry => (guarded slot entry).runState)) s = s := by induction entries with | nil => rfl @@ -115,7 +114,7 @@ theorem runSeq_guarded_locked_id (slot : Nat) (entries : List (Contract Unit)) packaged in the `Preserves` shape used by `ReentrancySpec` registries. -/ theorem guarded_preserves_on_locked (slot : Nat) (body : Contract Unit) (Inv : ContractState → Prop) : - ∀ s, s.transientStorage slot = 1 → Inv s → + ∀ s, s.transientStorage slot ≠ 0 → Inv s → Inv ((guarded slot body).runState s) := by intro s hlock hInv rw [guarded_reentry_blocked slot body s hlock] diff --git a/Verity/Proofs/Model/GeneratedEntrypointRegistry.lean b/Verity/Proofs/Model/GeneratedEntrypointRegistry.lean index 0a8d0a449..49e5256e4 100644 --- a/Verity/Proofs/Model/GeneratedEntrypointRegistry.lean +++ b/Verity/Proofs/Model/GeneratedEntrypointRegistry.lean @@ -41,7 +41,7 @@ theorem guardedPing_registered (adv : AdversaryModel) (ctx : CallbackContext) /-- The executable generated entrypoint is definitionally protected by the canonical source guard at the same slot used by the compiled dispatch guard. -/ theorem guardedPing_reentry_blocked (adv : AdversaryModel) (value : Uint256) - (state : ContractState) (hlock : state.transientStorage 0 = 1) : + (state : ContractState) (hlock : state.transientStorage 0 ≠ 0) : (guardedPing (ExecutableCallContext.ofAdversary adv) value).runState state = state := by apply Verity.Core.NonReentrantGuard.guarded_reentry_blocked exact hlock From ded1d3155be30db45fb8c2196b623bb40a8f9b32 Mon Sep 17 00:00:00 2001 From: Thomas Marchand Date: Tue, 8 Sep 2026 10:53:27 +0200 Subject: [PATCH 26/50] fix(reentrancy): isolate callback frame memory --- Verity/Core/Model/CallbackBridge.lean | 19 ++++++++++++++++++- 1 file changed, 18 insertions(+), 1 deletion(-) diff --git a/Verity/Core/Model/CallbackBridge.lean b/Verity/Core/Model/CallbackBridge.lean index 8c9063d2a..d37e618e5 100644 --- a/Verity/Core/Model/CallbackBridge.lean +++ b/Verity/Core/Model/CallbackBridge.lean @@ -63,6 +63,7 @@ def withCallbackContext (ctx : CallbackContext) (world : Verity.ContractState) : selfBalance := world.selfBalance + ctx.msgValue calldataSize := ctx.calldataSize calldata := ctx.calldata + memory := fun _ => 0 returndata := [] } def restoreCallbackContext (outer callbackResult : Verity.ContractState) : @@ -71,7 +72,9 @@ def restoreCallbackContext (outer callbackResult : Verity.ContractState) : sender := outer.sender msgValue := outer.msgValue calldataSize := outer.calldataSize - calldata := outer.calldata } + calldata := outer.calldata + memory := outer.memory + returndata := outer.returndata } def callbackTransition (ctx : CallbackContext) (entrypoint : Verity.ContractState → Verity.ContractState) : @@ -102,6 +105,10 @@ def callbackTransition (ctx : CallbackContext) (world : Verity.ContractState) : (withCallbackContext ctx world).returndata = [] := rfl +@[simp] theorem withCallbackContext_memory (ctx : CallbackContext) + (world : Verity.ContractState) : + (withCallbackContext ctx world).memory = (fun _ => 0) := rfl + @[simp] theorem callbackTransition_restores_sender (ctx : CallbackContext) (entrypoint : Verity.ContractState → Verity.ContractState) (outer : Verity.ContractState) : @@ -122,6 +129,16 @@ def callbackTransition (ctx : CallbackContext) (outer : Verity.ContractState) : (callbackTransition ctx entrypoint outer).calldataSize = outer.calldataSize := rfl +@[simp] theorem callbackTransition_restores_memory (ctx : CallbackContext) + (entrypoint : Verity.ContractState → Verity.ContractState) + (outer : Verity.ContractState) : + (callbackTransition ctx entrypoint outer).memory = outer.memory := rfl + +@[simp] theorem callbackTransition_restores_returndata (ctx : CallbackContext) + (entrypoint : Verity.ContractState → Verity.ContractState) + (outer : Verity.ContractState) : + (callbackTransition ctx entrypoint outer).returndata = outer.returndata := rfl + /-- Each mutable transition is some finite reentry schedule drawn from the registry. Static sites are unrestricted: `denoteCall` never commits their transitions, and `Conforms` separately pins them externally. -/ From c7e0fa6d097fc4d76b24d66b4a108b221ae03295 Mon Sep 17 00:00:00 2001 From: Thomas Marchand Date: Tue, 8 Sep 2026 11:27:52 +0200 Subject: [PATCH 27/50] fix(registry): preserve callback rollback semantics --- Verity/Core/Model/CallbackBridge.lean | 28 +++++++++++++++++++ Verity/Macro/Translate.lean | 6 ++-- .../Model/GeneratedEntrypointRegistry.lean | 8 +++--- 3 files changed, 36 insertions(+), 6 deletions(-) diff --git a/Verity/Core/Model/CallbackBridge.lean b/Verity/Core/Model/CallbackBridge.lean index d37e618e5..b83306ae3 100644 --- a/Verity/Core/Model/CallbackBridge.lean +++ b/Verity/Core/Model/CallbackBridge.lean @@ -81,6 +81,34 @@ def callbackTransition (ctx : CallbackContext) Verity.ContractState → Verity.ContractState := fun outer => restoreCallbackContext outer (entrypoint (withCallbackContext ctx outer)) +/-- Run an executable callback while retaining its success/revert outcome. +Successful callbacks commit their state after restoring the caller's ambient +frame; reverting callbacks roll back the entire callback, including the value +credit installed on entry. -/ +def callbackContractTransition (ctx : CallbackContext) + (entrypoint : Verity.Contract α) : + Verity.ContractState → Verity.ContractState := + fun outer => + match entrypoint.run (withCallbackContext ctx outer) with + | .success _ callbackResult => restoreCallbackContext outer callbackResult + | .revert _ _ => outer + +@[simp] theorem callbackContractTransition_success (ctx : CallbackContext) + (entrypoint : Verity.Contract α) (outer callbackResult : Verity.ContractState) + (value : α) + (hrun : entrypoint.run (withCallbackContext ctx outer) = + Verity.ContractResult.success value callbackResult) : + callbackContractTransition ctx entrypoint outer = + restoreCallbackContext outer callbackResult := by + simp [callbackContractTransition, hrun] + +@[simp] theorem callbackContractTransition_revert (ctx : CallbackContext) + (entrypoint : Verity.Contract α) (outer : Verity.ContractState) (message : String) + (hrun : entrypoint.run (withCallbackContext ctx outer) = + Verity.ContractResult.revert message (withCallbackContext ctx outer)) : + callbackContractTransition ctx entrypoint outer = outer := by + simp [callbackContractTransition, hrun] + @[simp] theorem withCallbackContext_sender (ctx : CallbackContext) (world : Verity.ContractState) : (withCallbackContext ctx world).sender = ctx.sender := rfl diff --git a/Verity/Macro/Translate.lean b/Verity/Macro/Translate.lean index 81dfa8c78..79a77a769 100644 --- a/Verity/Macro/Translate.lean +++ b/Verity/Macro/Translate.lean @@ -5824,8 +5824,10 @@ def mkFunctionCommandsPublic applied ← `($applied $registryParam:ident) let mut registryBody : Term ← `(($transition:ident : Verity.ContractState → Verity.ContractState) = - Compiler.CompilationModel.DenoteExternalCalls.callbackTransition - $context:ident ($applied).runState) + Compiler.CompilationModel.DenoteExternalCalls.callbackContractTransition + $context:ident $applied) + if !fn.isPayable then + registryBody ← `(($context:ident).msgValue = 0 ∧ $registryBody) for (paramIdent, paramTy) in registryParams.reverse do registryBody ← `(∃ $paramIdent:ident : $paramTy, $registryBody) registryBody ← diff --git a/Verity/Proofs/Model/GeneratedEntrypointRegistry.lean b/Verity/Proofs/Model/GeneratedEntrypointRegistry.lean index 49e5256e4..c320aed0c 100644 --- a/Verity/Proofs/Model/GeneratedEntrypointRegistry.lean +++ b/Verity/Proofs/Model/GeneratedEntrypointRegistry.lean @@ -31,12 +31,12 @@ open Compiler.CompilationModel.DenoteExternalCalls /-- The generated registry uses its explicit adversary at the external-call entrypoint; there is no `.stub` compatibility path in this theorem surface. -/ theorem guardedPing_registered (adv : AdversaryModel) (ctx : CallbackContext) - (value : Uint256) : + (value : Uint256) (hvalue : ctx.msgValue = 0) : entrypointRegistry adv - (callbackTransition ctx - (guardedPing_registry (ExecutableCallContext.ofAdversary adv) value).runState) := by + (callbackContractTransition ctx + (guardedPing_registry (ExecutableCallContext.ofAdversary adv) value)) := by left - exact ⟨ctx, value, rfl⟩ + exact ⟨ctx, value, hvalue, rfl⟩ /-- The executable generated entrypoint is definitionally protected by the canonical source guard at the same slot used by the compiled dispatch guard. -/ From c84ea249c87241d49ea34849c5631ffbd97453cc Mon Sep 17 00:00:00 2001 From: "github-actions[bot]" Date: Tue, 15 Sep 2026 12:00:01 +0200 Subject: [PATCH 28/50] chore: auto-refresh derived artifacts --- PrintAxioms.lean | 8 +++++++- 1 file changed, 7 insertions(+), 1 deletion(-) diff --git a/PrintAxioms.lean b/PrintAxioms.lean index da317732e..966120847 100644 --- a/PrintAxioms.lean +++ b/PrintAxioms.lean @@ -39,6 +39,7 @@ import Contracts.VaultFromSolidity.Proofs.ExecutionProof import Verity.Proofs.CheckedExternalCallConsumer import Verity.Proofs.LoopSimulationResultAware import Verity.Proofs.Model.CommonExternalCallEquivalence +import Verity.Proofs.Model.GeneratedEntrypointRegistry import Verity.Proofs.Stdlib.Automation import Verity.Proofs.Stdlib.ListSum import Verity.Proofs.Stdlib.MappingAutomation @@ -729,6 +730,11 @@ end Verity.AxiomAudit Contracts.legacyStringSafeTransfer_eq_stub Contracts.legacyStringSafeTransferFrom_eq_stub + -- Verity/Proofs/Model/GeneratedEntrypointRegistry.lean + Contracts.ReentrancyRelyGuarantee.GeneratedRegistry.guardedPing_registered + Contracts.ReentrancyRelyGuarantee.GeneratedRegistry.guardedPing_reentry_blocked + Contracts.ReentrancyRelyGuarantee.generated_registry_callback_preserves + -- Verity/Proofs/Stdlib/Automation.lean Verity.Proofs.Stdlib.Automation.isSuccess_success Verity.Proofs.Stdlib.Automation.isSuccess_revert @@ -7526,4 +7532,4 @@ end Verity.AxiomAudit Compiler.Proofs.YulGeneration.YulTransaction.ofIR_args ] --- Total: 6961 theorems/lemmas (4971 public, 1990 private, 0 sorry'd) +-- Total: 6964 theorems/lemmas (4974 public, 1990 private, 0 sorry'd) From b1109707498f3d14218199fb03ecc5f8d87af7eb Mon Sep 17 00:00:00 2001 From: Thomas Marchand Date: Tue, 15 Sep 2026 12:04:11 +0200 Subject: [PATCH 29/50] fix(macro): resolve adversarial overload targets (PR2406 PR4 registry/guard) - Non-guarded adversarial overloads use resolved/mangled helper.ident (r3956459377) - Resolve original nested external-call args before hoisting for sound local typing (r3956459383) - Qualify Contracts.ExecutableCallContext.ofAdversary and GeneratedEntrypointRegistry usage - PR4-only: registry/guard/CallbackBounded scope preserved - Validation pending remote (Nippur) + Fable; push before Fable authorized --- Verity/Macro/Translate.lean | 65 ++++++++++++++----- .../Model/GeneratedEntrypointRegistry.lean | 4 +- 2 files changed, 50 insertions(+), 19 deletions(-) diff --git a/Verity/Macro/Translate.lean b/Verity/Macro/Translate.lean index 79a77a769..4fa495b57 100644 --- a/Verity/Macro/Translate.lean +++ b/Verity/Macro/Translate.lean @@ -1909,7 +1909,7 @@ private def adversaryModelTypeTerm : CommandElabM Term := `(Compiler.CompilationModel.DenoteExternalCalls.AdversaryModel) private def executableCallContextTypeTerm : CommandElabM Term := - `(ExecutableCallContext) + `(Contracts.ExecutableCallContext) private def mkContractFnTypeWithAdversary (params : Array ParamDecl) (retTy : ValueType) : CommandElabM Term := do @@ -2421,7 +2421,8 @@ private def threadHelperApp? (registryOnlyHelpers : Array FunctionDecl) (params : Array ParamDecl) (locals : Array TypedLocal) (name : Ident) (args : Array Term) - (adv : Term) : CommandElabM (Option Term) := do + (adv : Term) + (originalArgsForOverload : Option (Array Term) := none) : CommandElabM (Option Term) := do let matchesHelper := fun (fn : FunctionDecl) => (fn.name == toString name.getId || fn.ident.getId == name.getId || (toString name.getId).endsWith ("." ++ fn.name)) && @@ -2434,7 +2435,8 @@ private def threadHelperApp? if exactCandidates.size <= 1 then pure (exactCandidates[0]? <|> helpers.find? matchesHelper) else - let app ← helperCall name args + let resolveArgs := originalArgsForOverload.getD args + let app ← helperCall name resolveArgs try pure ((← resolveLocalFunctionApp? fields constDecls immutableDecls externalDecls helpers params locals app).map (·.1)) @@ -2461,7 +2463,7 @@ private def threadHelperApp? matchesExactHelper helper then mkSuffixedIdent helper.ident "_unguarded" else - pure name + pure helper.ident if matchesExactHelper helper && adversarialHelpers.any (fun candidate => functionSignatureKey candidate == functionSignatureKey helper) then some <$> helperCallWithAdv target args adv @@ -2885,19 +2887,38 @@ private partial def threadAdversaryThroughExecutableSyntax let rewrittenBody : TSyntax ``Lean.Parser.Term.doSeq := ⟨bodyRaw⟩ pure (#[], ← `(term| do $rewrittenBody)) | `(term| $name:ident($[$args:term],*)) => - let mut binds : Array (Ident × Term) := #[] - let mut rewrittenArgs : Array Term := #[] - for arg in args do - let (inner, rewritten) ← hoistNested true arg - binds := binds ++ inner - rewrittenArgs := rewrittenArgs.push rewritten + let original := args.map fun a => (⟨a.raw⟩ : Term) + -- Resolve overloads using original source arguments so mangled helper.idents + -- for non-guarded adversarial overloads are selected before hoisting rewrites the args. match ← threadHelperApp? fields constDecls immutableDecls externalDecls - helpers adversarialHelpers registryOnlyHelpers params locals name rewrittenArgs adv with - | some app => pure (binds, app) + helpers adversarialHelpers registryOnlyHelpers params locals name original adv with + | some _selectedApp => + -- Now hoist nested external-call arguments (or type generated locals soundly), + -- then thread the selected helper using the hoisted arguments. + let mut binds : Array (Ident × Term) := #[] + let mut hoisted : Array Term := #[] + for arg in args do + let (inner, rewritten) ← hoistNested true arg + binds := binds ++ inner + hoisted := hoisted.push rewritten + match ← threadHelperApp? fields constDecls immutableDecls externalDecls + helpers adversarialHelpers registryOnlyHelpers params locals name hoisted adv with + | some app => pure (binds, app) + | none => + let mut app : Term := ⟨name.raw⟩ + for h in hoisted do + app ← `(term| $app $h) + pure (binds, app) | none => + let mut binds : Array (Ident × Term) := #[] + let mut hoisted : Array Term := #[] + for arg in args do + let (inner, rewritten) ← hoistNested true arg + binds := binds ++ inner + hoisted := hoisted.push rewritten let mut app : Term := ⟨name.raw⟩ - for arg in rewrittenArgs do - app ← `(term| $app $arg) + for h in hoisted do + app ← `(term| $app $h) pure (binds, app) | `(term| if $cond:term then $thenValue:term else $elseValue:term) => let (condBinds, rewrittenCond) ← hoistNested true cond @@ -3032,9 +3053,19 @@ private partial def threadAdversaryThroughExecutableSyntax (← `(doElem| let $pat:term ← (totalSupply (externalArgAddress $rewrittenToken) $adv))) | `(doElem| let $name:ident ← $fn:ident($[$args:term],*)) => + let original := args.map fun a => (⟨a.raw⟩ : Term) + -- Resolve overload using original source args (r3956459377), hoist after selection (r3956459383) match ← threadHelperApp? fields constDecls immutableDecls externalDecls - helpers adversarialHelpers registryOnlyHelpers params locals fn args adv with - | some app => `(doElem| let $name ← $app:term) + helpers adversarialHelpers registryOnlyHelpers params locals fn original adv with + | some _ => + let mut hoisted : Array Term := #[] + for a in args do + let (_, h) ← hoistNested true a + hoisted := hoisted.push h + match ← threadHelperApp? fields constDecls immutableDecls externalDecls + helpers adversarialHelpers registryOnlyHelpers params locals fn hoisted adv with + | some app => `(doElem| let $name ← $app:term) + | none => recurseChildren | none => recurseChildren | `(doElem| let $name:ident ← $fn:ident $args:term*) => let original := args.map fun arg => (⟨arg.raw⟩ : Term) @@ -5813,7 +5844,7 @@ def mkFunctionCommandsPublic let transition : Ident := ⟨transitionIdent.raw⟩ let context : Ident := ⟨contextIdent.raw⟩ let mut applied : Term := registryId - applied ← `($applied (ExecutableCallContext.ofAdversary $registryAdv:ident)) + applied ← `($applied (Contracts.ExecutableCallContext.ofAdversary $registryAdv:ident)) let mut registryParams : Array (Ident × Term) := #[] for param in fn.params do let paramTy ← contractValueTypeTerm param.ty diff --git a/Verity/Proofs/Model/GeneratedEntrypointRegistry.lean b/Verity/Proofs/Model/GeneratedEntrypointRegistry.lean index c320aed0c..3223e0ffb 100644 --- a/Verity/Proofs/Model/GeneratedEntrypointRegistry.lean +++ b/Verity/Proofs/Model/GeneratedEntrypointRegistry.lean @@ -34,7 +34,7 @@ theorem guardedPing_registered (adv : AdversaryModel) (ctx : CallbackContext) (value : Uint256) (hvalue : ctx.msgValue = 0) : entrypointRegistry adv (callbackContractTransition ctx - (guardedPing_registry (ExecutableCallContext.ofAdversary adv) value)) := by + (guardedPing_registry (Contracts.ExecutableCallContext.ofAdversary adv) value)) := by left exact ⟨ctx, value, hvalue, rfl⟩ @@ -42,7 +42,7 @@ theorem guardedPing_registered (adv : AdversaryModel) (ctx : CallbackContext) canonical source guard at the same slot used by the compiled dispatch guard. -/ theorem guardedPing_reentry_blocked (adv : AdversaryModel) (value : Uint256) (state : ContractState) (hlock : state.transientStorage 0 ≠ 0) : - (guardedPing (ExecutableCallContext.ofAdversary adv) value).runState state = state := by + (guardedPing (Contracts.ExecutableCallContext.ofAdversary adv) value).runState state = state := by apply Verity.Core.NonReentrantGuard.guarded_reentry_blocked exact hlock From 031967edaee43ad32897c509116ede1b94ab7c86 Mon Sep 17 00:00:00 2001 From: Thomas Marchand Date: Tue, 15 Sep 2026 13:13:56 +0200 Subject: [PATCH 30/50] =?UTF-8?q?fix(macro):=20thread=20hoist=20binds=20fo?= =?UTF-8?q?r=20let-bound=20overloaded=20adversarial=20calls=20(x=E2=9C=9D?= =?UTF-8?q?=20hygiene)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit In threadAdversaryThroughExecutableSyntax, the arm resolved the overload using original args then hoisted, but discarded the binds returned by hoistNested and emitted a bare doElem. When an argument contained a live externalCall (e.g. trustedEntry(externalCall echo [x]) in SecurityCombos), hoistNested produced fresh binders that were never wrapped; downstream elaboration saw an inaccessible . Collect the binds and use the existing wrapBinds helper (consistent with the adjacent and external-call arms). PR4-only (registry/guard/CallbackBounded/adversarial overload resolution). No new theorems, no PR5/6 surface. Fixes remote receipt 76be660b (head b66932ed) targeted build failure at Contracts/Smoke/SecurityCombos.lean:255:25. --- Verity/Macro/Translate.lean | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/Verity/Macro/Translate.lean b/Verity/Macro/Translate.lean index 4fa495b57..1788020ca 100644 --- a/Verity/Macro/Translate.lean +++ b/Verity/Macro/Translate.lean @@ -3058,13 +3058,15 @@ private partial def threadAdversaryThroughExecutableSyntax match ← threadHelperApp? fields constDecls immutableDecls externalDecls helpers adversarialHelpers registryOnlyHelpers params locals fn original adv with | some _ => + let mut binds : Array (Ident × Term) := #[] let mut hoisted : Array Term := #[] for a in args do - let (_, h) ← hoistNested true a + let (inner, h) ← hoistNested true a + binds := binds ++ inner hoisted := hoisted.push h match ← threadHelperApp? fields constDecls immutableDecls externalDecls helpers adversarialHelpers registryOnlyHelpers params locals fn hoisted adv with - | some app => `(doElem| let $name ← $app:term) + | some app => wrapBinds binds (← `(doElem| let $name ← $app:term)) | none => recurseChildren | none => recurseChildren | `(doElem| let $name:ident ← $fn:ident $args:term*) => From a27da82eb9105150a9b3206aefa1f1c8b24adccc Mon Sep 17 00:00:00 2001 From: Thomas Marchand Date: Tue, 15 Sep 2026 16:45:58 +0200 Subject: [PATCH 31/50] fix(macro): hoist open of DenoteExternalCalls to outer PR4 namespace for GeneratedEntrypointRegistry hygiene (PrintAxioms gate) --- Verity/Proofs/Model/GeneratedEntrypointRegistry.lean | 5 +---- scripts/measure.sh | 8 ++++++++ 2 files changed, 9 insertions(+), 4 deletions(-) create mode 100755 scripts/measure.sh diff --git a/Verity/Proofs/Model/GeneratedEntrypointRegistry.lean b/Verity/Proofs/Model/GeneratedEntrypointRegistry.lean index 3223e0ffb..00e8c2e94 100644 --- a/Verity/Proofs/Model/GeneratedEntrypointRegistry.lean +++ b/Verity/Proofs/Model/GeneratedEntrypointRegistry.lean @@ -6,6 +6,7 @@ namespace Contracts.ReentrancyRelyGuarantee open Contracts open Verity hiding pure bind +open Compiler.CompilationModel.DenoteExternalCalls /-! Focused generated consumer for the registry/guard boundary. It contains an actual mutable external-call window, so the executable entrypoint must take @@ -26,8 +27,6 @@ verity_contract GeneratedRegistry where namespace GeneratedRegistry -open Compiler.CompilationModel.DenoteExternalCalls - /-- The generated registry uses its explicit adversary at the external-call entrypoint; there is no `.stub` compatibility path in this theorem surface. -/ theorem guardedPing_registered (adv : AdversaryModel) (ctx : CallbackContext) @@ -48,8 +47,6 @@ theorem guardedPing_reentry_blocked (adv : AdversaryModel) (value : Uint256) end GeneratedRegistry -open Compiler.CompilationModel.DenoteExternalCalls - /-- `ReentrancyRelyGuarantee` consumes the emitted registry at the restricted callback boundary. Contract-specific preservation obligations remain with authors; this PR establishes only the generated registry/guard connection. -/ diff --git a/scripts/measure.sh b/scripts/measure.sh new file mode 100755 index 000000000..3fb2336b4 --- /dev/null +++ b/scripts/measure.sh @@ -0,0 +1,8 @@ +#!/bin/sh +set -eu +START=$(date +%s) +echo "START: $(date -Iseconds)" +lake build 2>&1 | tail -20 +END=$(date +%s) +echo "DURATION: $((END-START))s" +echo "END: $(date -Iseconds)" From 63b1d14216fae8e756ce0b94f7cd33243e40aef4 Mon Sep 17 00:00:00 2001 From: Thomas Marchand Date: Thu, 17 Sep 2026 22:01:31 +0200 Subject: [PATCH 32/50] fix(macro): route registry window helpers through _registry variants Registry executables were suffixing only non-window adversarial helpers, so entry_registry called public hop which stubbed nested view helpers. Route every adversarial helper, including window helpers, to _registry/_registry_unguarded inside registry bodies. Add a regression fixture. Sync trust docs for tload guard Yul, landed macro registry, and ofAdversary target 0/value 0. --- Contracts/Common.lean | 5 ++ TRUST_ASSUMPTIONS.md | 16 +++-- Verity/Macro/Translate.lean | 14 ++-- .../Model/GeneratedEntrypointRegistry.lean | 72 +++++++++++++++++++ docs/ROADMAP.md | 2 +- 5 files changed, 100 insertions(+), 9 deletions(-) diff --git a/Contracts/Common.lean b/Contracts/Common.lean index 272b520e9..d84843fe6 100644 --- a/Contracts/Common.lean +++ b/Contracts/Common.lean @@ -687,6 +687,11 @@ structure ExecutableCallContext where adversary : AdversaryModel resolve : String → Nat → Option Compiler.CompilationModel.DenoteFunctionCalls.LinkedExternal +/-- Bind an explicit adversary while pinning every resolved linked call to +`target = 0` and `value = 0`. This is the registry/executable convenience +boundary: the adversary is live, but link-time callee address and ETH value +are the zero defaults. Callers that need a real target or nonzero value must +supply `resolve` themselves (`ofCallEnv` or a custom context). -/ def ExecutableCallContext.ofAdversary (adv : AdversaryModel) : ExecutableCallContext := { adversary := adv resolve := fun _ fallbackSiteId => diff --git a/TRUST_ASSUMPTIONS.md b/TRUST_ASSUMPTIONS.md index 8b69d3b04..d70bf223d 100644 --- a/TRUST_ASSUMPTIONS.md +++ b/TRUST_ASSUMPTIONS.md @@ -391,6 +391,11 @@ of byte-for-byte EVM ABI layout. Trust boundaries of that plane: and no-result stubs use their inhabited default. Executable-plane theorems about call *outcomes* are therefore claims about the stub, not about a real callee; adversarial reasoning lives in the model plane (`DenoteExternalCalls`). +- **`ExecutableCallContext.ofAdversary` pins `target = 0` and `value = 0`.** + The registry predicate applies generated `*_registry` executables through + this helper so the adversary is explicit while link-time callee address and + ETH value stay at the zero boundary. A nonzero target or value requires + `ofCallEnv` or a custom `resolve`; ofAdversary is not a general linker. - **`externalCallWords` (pure expression form) does not journal.** It is not monadic, so `externalCall name [args]` used as a pure expression remains observationally silent; only the monadic forms journal. Specs that need @@ -432,7 +437,7 @@ of byte-for-byte EVM ABI layout. Trust boundaries of that plane: ### Reentrancy Guard (`nonreentrant(lockField)`) Functions annotated `nonreentrant(lockField)` are compiled with a **transient-storage** reentrancy guard prologue (#1893): an -`if eq(tload(lockSlot), 1) { revert(0, 0) }; tstore(lockSlot, 1)` pair runs +`if tload(lockSlot) { revert(0, 0) }; tstore(lockSlot, 1)` pair runs before any user-authored Yul. Transient storage (EIP-1153, Cancun+) auto-clears at end-of-transaction, so the guard does not need an explicit release path — early `return`, `revert`, or panic cannot leak the lock across transactions. @@ -449,7 +454,7 @@ reentry window is closed at the model level. On the compiled side, prologue/release statements under the IR interpreter: locked entry reverts untouched, free entry acquires the lock and changes nothing else, the spliced release resets it (acquire/release round-trips the transient store), and the -Yul decision `eq(lock,1)` agrees with the model's `lock ≠ 0` on reachable +Yul decision `if tload(slot)` (nonzero is true) agrees with the model's `lock ≠ 0` on reachable binary lock values. `Compiler.Proofs.IRGeneration.SpliceSimulation` now proves the full guarded unit end to end for the loop/switch-free fragment with compiler-emitted exits: the general splice simulation (`execIRStmts_spliced`), both @@ -517,8 +522,11 @@ global invariant `I : ContractState → Prop`. externally reachable state transformer an adversary can invoke during a reentry window. Omitting a reachable entrypoint voids the guarantee (analogous to declaring the lock field for `nonreentrant`). The - macro-emitted entrypoint registry is the intended source of this list; until - that emission lands, the list is author-supplied. + macro-emitted entrypoint registry (`entrypointRegistry` and per-function + `*_registry` / `*_registry_unguarded` executables generated by + `verity_contract`) is the source of this list on the macro path. + Author-supplied lists remain only for hand-written `ReentrancySpec` + consumers. 2. **Adversary-model fidelity** — reentry is modeled as an arbitrary `ContractState → ContractState` over *this* contract's persistent channels (`adv` in `reentrantCall`). This captures self- and cross-contract reentry diff --git a/Verity/Macro/Translate.lean b/Verity/Macro/Translate.lean index 1788020ca..0c4f31faf 100644 --- a/Verity/Macro/Translate.lean +++ b/Verity/Macro/Translate.lean @@ -2452,6 +2452,10 @@ private def threadHelperApp? -- is not a local helper call. Leave it to the recursive traversal so -- linked calls nested in its arguments still receive the adversary. return none + -- `registryOnlyHelpers` is the set that must be called as `_registry` + -- / `_registry_unguarded`. Public bodies pass `#[]`; registry bodies pass + -- every adversarial helper, including window-opening ones, so a nested + -- `hop` cannot drop into the stub-only public helper. let registryOnly := registryOnlyHelpers.any (fun candidate => functionSignatureKey candidate == functionSignatureKey helper) let target ← @@ -5756,9 +5760,6 @@ def mkFunctionCommandsPublic let windowNames := windowHelpers.map (·.name) let callsWindow ← syntaxCallsAnyHelper windowNames modelFn.body.raw let opensReentrancyWindow := directlyOpensReentrancyWindow || callsWindow - let registryOnlyHelpers := adversarialHelpers.filter fun helper => - !windowHelpers.any (fun candidate => - functionSignatureKey candidate == functionSignatureKey helper) -- Keep the generated binder hygienic: source parameters and locals are allowed -- to use `_adv` without capturing the adversary threaded into rewritten calls. let advIdent ← Lean.Elab.Term.mkFreshIdent (mkIdentFrom fn.ident `_adv).raw @@ -5772,8 +5773,13 @@ def mkFunctionCommandsPublic mkContractFnType fn.params fn.returnTy let publicExecutableBody := ⟨← threadAdversaryThroughExecutableSyntax fields constDecls immutableDecls externalDecls functions windowHelpers #[] fn.params #[] advTerm fnExecutableBody.raw⟩ + -- Registry executables must route every adversarial helper, including + -- window-opening helpers, to `_registry` / `_registry_unguarded`. Restricting + -- the suffix to non-window helpers let `entry_registry` call public `hop`, + -- which then used stub-only nested view helpers and under-approximated the + -- compiled callee-controlled ECM. let registryExecutableBody := ⟨← threadAdversaryThroughExecutableSyntax fields constDecls immutableDecls - externalDecls functions adversarialHelpers registryOnlyHelpers fn.params #[] + externalDecls functions adversarialHelpers adversarialHelpers fn.params #[] (⟨advIdent.raw⟩ : Term) fnExecutableBody.raw⟩ let mut extraExecutableCmds : Array Cmd := #[] if fn.nonReentrantLock.isSome && fn.reentrancyTrusted then diff --git a/Verity/Proofs/Model/GeneratedEntrypointRegistry.lean b/Verity/Proofs/Model/GeneratedEntrypointRegistry.lean index 00e8c2e94..4ca5d2171 100644 --- a/Verity/Proofs/Model/GeneratedEntrypointRegistry.lean +++ b/Verity/Proofs/Model/GeneratedEntrypointRegistry.lean @@ -47,6 +47,78 @@ theorem guardedPing_reentry_blocked (adv : AdversaryModel) (value : Uint256) end GeneratedRegistry +/-! Regression for registry window-helper routing: `readBal` is adversarial +(view/staticcall ECM) but does not open a reentrancy window, while `hop` +opens a window and then calls `readBal`. `entry_registry` must call +`hop_registry` (which calls `readBal_registry`) rather than public `hop` +(which uses stub-only `readBal` and under-approximates a callee-controlled +view ECM). -/ +verity_contract RegistryWindowHelperRouting where + storage + last : Uint256 := slot 0 + interfaces + interface IToken where + function balanceOf(Address) view returns (Uint256) + end + linked_externals + external ping(Uint256) -> (Uint256) + + function view readBal (token : IToken, who : Address) : Uint256 := do + let observed ← token.balanceOf who + return observed + + function reentrancy_trusted hop (token : IToken, who : Address) : Uint256 := do + let _ack := externalCall "ping" [0] + let observed ← readBal token who + return observed + + function allow_post_interaction_writes reentrancy_trusted entry + (token : IToken, who : Address) : Uint256 := do + let observed ← hop token who + setStorage last observed + return observed + +namespace RegistryWindowHelperRouting + +/-- Distinctive view-call adversary: staticcall sites return 42 instead of the +deterministic stub word. Public `hop` ignores this because it calls stub-only +`readBal`; `hop_registry` / `entry_registry` must observe 42. -/ +def distinctiveViewAdv : AdversaryModel where + stateTransition := fun _ state => state + result := fun site world => + if site.kind = .staticcall then .success [42] + else AdversaryModel.stub.result site world + gasUsed := fun _ _ => 0 + +def distinctiveCtx : Contracts.ExecutableCallContext := + Contracts.ExecutableCallContext.ofAdversary distinctiveViewAdv + +/-- Public `hop` still uses stub-only `readBal` (no adversary). -/ +def hopPublicSeesStub : Bool := + match (hop distinctiveCtx 0 0).run Verity.defaultState with + | .success value _ => !(value == 42) + | _ => false + +example : hopPublicSeesStub = true := by decide + +/-- `hop_registry` routes the nested view helper through `readBal_registry`. -/ +def hopRegistrySeesAdversary : Bool := + match (hop_registry distinctiveCtx 0 0).run Verity.defaultState with + | .success value _ => value == 42 + | _ => false + +example : hopRegistrySeesAdversary = true := by decide + +/-- `entry_registry` must call `hop_registry`, not public `hop`. -/ +def entryRegistrySeesAdversary : Bool := + match (entry_registry distinctiveCtx 0 0).run Verity.defaultState with + | .success value _ => value == 42 + | _ => false + +example : entryRegistrySeesAdversary = true := by decide + +end RegistryWindowHelperRouting + /-- `ReentrancyRelyGuarantee` consumes the emitted registry at the restricted callback boundary. Contract-specific preservation obligations remain with authors; this PR establishes only the generated registry/guard connection. -/ diff --git a/docs/ROADMAP.md b/docs/ROADMAP.md index 1b5a1090e..279467424 100644 --- a/docs/ROADMAP.md +++ b/docs/ROADMAP.md @@ -257,7 +257,7 @@ Priority work for Verity core: | P1 | `sha256` / `sha256Packed` helper | Avoid hand-rolled SHA-256 precompile calls in public-signal construction. | `Compiler.Modules.Precompiles.sha256Memory` covers existing memory slices, with `sha256` as a short alias; `Compiler.Modules.Hashing.sha256PackedWords` covers static-word packed preimages, with `sha256Packed` as a short alias; `Compiler.Modules.Hashing.sha256PackedStaticSegments` covers static 1- to 32-byte segments. SHA-256 helpers route through precompile 0x02 with failure reverts and generated-Yul/trust-report tests. | | P1 | BN254 curve precompile ECMs | Avoid hand-rolled assembly for Groth16-style verifiers and other zkSNARK postcondition checks at the EVM boundary. | `Compiler.Modules.Precompiles.bn256Add` (0x06), `bn256ScalarMul` (0x07), and `bn256Pairing` (0x08) lower to staticcall against the EIP-196/EIP-197 precompiles, bind output coordinates / boolean word from scratch memory, revert on precompile failure, and surface a single `evm_bn256_*_precompile` trust assumption each; generated-Yul + trust-report smoke tests live in `Compiler/CompilationModelFeatureTest.lean`. | | P1 | `keccak256_lit` compile-time literal sugar | Make ERC-7201 namespaces and other Keccak-of-string constants safe and reviewable inside `verity_contract` bodies without ad-hoc Lean. | `Verity.Macro.KeccakLit` exposes `keccak256_nat` / `keccak256_lit` backed by the in-tree pure Keccak engine (`Compiler.Keccak.Sponge`) so authors can write `constants STORAGE_NAMESPACE : Uint256 := keccak256_lit "MyContract.storage.v0"`; the helpers are pure Lean definitions (no new trust assumption) with `native_decide`-checked test vectors against the official Keccak-256 empty-string digest. The follow-on `keccakString ""` term form (`Verity.Macro.KeccakString`, #1973) computes the digest at macro-expansion time and emits a `Uint256` numeric literal directly; it is parser-restricted to string literals (non-literal arguments are rejected at parse time) and is pattern-matched by the `verity_contract` translator, so EIP-712 type hashes, ERC-7201 namespaces, and event topic constants can be expressed without storage reads, without runtime hashing, and without per-author copies of the digest. | -| P2 | Real `nonreentrant` guard semantics (#1893) | Upgrade the `nonreentrant(lockField)` annotation from a metadata/proof hook to a synthesised runtime guard so contracts can safely write state after external calls within reentrancy-protected entry points. | `Compiler.CompilationModel.Dispatch.attachNonReentrantGuard` (#1893) prepends a **transient-storage** acquire prologue — `if eq(tload(lockSlot), 1) { revert(0, 0) }; tstore(lockSlot, 1)` — immediately after parameter loading for any `nonreentrant(lockField)` external. Transient storage (TLOAD / TSTORE, EIP-1153, Cancun+) auto-clears at end-of-transaction, so no exit-path cleanup is needed and early `return` / `revert` / panic cannot leak the lock across transactions. CEI enforcement in `Compiler.CompilationModel.Validation.validateFunctionSpec` is now lifted for `nonReentrantLock.isSome` functions because the synthesised guard closes the post-interaction-write reentry window at runtime. The `validateNonReentrantForkCompatibility` pre-check (#1968) rejects any contract carrying a `nonreentrant()` annotation when the targeted EVM fork predates Cancun, so the synthesised TLOAD/TSTORE opcodes cannot be silently emitted against a chain that does not expose them. Kept as a post-`compileFunctionSpec` transformation so the IR-generation proof modules continue to characterise the underlying body shape without a nonReentrantLock case split; the first version sits outside `SupportedSpec` (proof obligations for guarded specs are deferred). | +| P2 | Real `nonreentrant` guard semantics (#1893) | Upgrade the `nonreentrant(lockField)` annotation from a metadata/proof hook to a synthesised runtime guard so contracts can safely write state after external calls within reentrancy-protected entry points. | `Compiler.CompilationModel.Dispatch.attachNonReentrantGuard` (#1893) prepends a **transient-storage** acquire prologue — `if tload(lockSlot) { revert(0, 0) }; tstore(lockSlot, 1)` — immediately after parameter loading for any `nonreentrant(lockField)` external. Transient storage (TLOAD / TSTORE, EIP-1153, Cancun+) auto-clears at end-of-transaction, so no exit-path cleanup is needed and early `return` / `revert` / panic cannot leak the lock across transactions. CEI enforcement in `Compiler.CompilationModel.Validation.validateFunctionSpec` is now lifted for `nonReentrantLock.isSome` functions because the synthesised guard closes the post-interaction-write reentry window at runtime. The `validateNonReentrantForkCompatibility` pre-check (#1968) rejects any contract carrying a `nonreentrant()` annotation when the targeted EVM fork predates Cancun, so the synthesised TLOAD/TSTORE opcodes cannot be silently emitted against a chain that does not expose them. Kept as a post-`compileFunctionSpec` transformation so the IR-generation proof modules continue to characterise the underlying body shape without a nonReentrantLock case split; the first version sits outside `SupportedSpec` (proof obligations for guarded specs are deferred). | | P2 | BN254 scalar field helper | Improve readability of circuit-facing reductions. | `Verity.Stdlib.Math` exposes documented `SNARK_SCALAR_FIELD` and `modField` helpers with basic simp lemmas. | Already-supported items that should not become new roadmap work: From 348447d12a7f76036adadc905aa91fdd522ba638 Mon Sep 17 00:00:00 2001 From: "github-actions[bot]" Date: Fri, 18 Sep 2026 00:50:48 +0100 Subject: [PATCH 33/50] chore: auto-refresh derived artifacts --- PrintAxioms.lean | 47 ++++++++++++++++++++++++++++++++++++++++++++++- 1 file changed, 46 insertions(+), 1 deletion(-) diff --git a/PrintAxioms.lean b/PrintAxioms.lean index d7d062307..6afa3fb81 100644 --- a/PrintAxioms.lean +++ b/PrintAxioms.lean @@ -41,6 +41,7 @@ import Verity.Proofs.LoopSimulationResultAware import Verity.Proofs.Model.CommonExternalCallEquivalence import Verity.Proofs.Model.GeneratedEntrypointRegistry import Verity.Proofs.Stdlib.Automation +import Verity.Proofs.Stdlib.Int256 import Verity.Proofs.Stdlib.ListSum import Verity.Proofs.Stdlib.MappingAutomation import Verity.Proofs.Stdlib.Math @@ -845,6 +846,50 @@ end Verity.AxiomAudit Verity.Proofs.Stdlib.Automation.require_beq_isSuccess_false_iff_ne Verity.Proofs.Stdlib.Automation.owner_guard_success_implies_storageAddr_eq_sender + -- Verity/Proofs/Stdlib/Int256.lean + -- Verity.Proofs.Stdlib.Int256.modulus_def -- private + -- Verity.Proofs.Stdlib.Int256.natCast_emod_of_lt -- private + -- Verity.Proofs.Stdlib.Int256.max_sub_min -- private + -- Verity.Proofs.Stdlib.Int256.maxValue_nat -- private + -- Verity.Proofs.Stdlib.Int256.natAbs_natCast -- private + Verity.Proofs.Stdlib.Int256.toInt_emod + -- Verity.Proofs.Stdlib.Int256.emod_neg_congr -- private + Verity.Proofs.Stdlib.Int256.inRange_eq_of_emod_eq + Verity.Proofs.Stdlib.Int256.toInt_add_of_inRange + Verity.Proofs.Stdlib.Int256.toInt_mul_of_inRange + -- Verity.Proofs.Stdlib.Int256.sub_word -- private + -- Verity.Proofs.Stdlib.Int256.sub_word_emod -- private + Verity.Proofs.Stdlib.Int256.toInt_sub_of_inRange + -- Verity.Proofs.Stdlib.Int256.neg_word -- private + -- Verity.Proofs.Stdlib.Int256.neg_maxValue -- private + -- Verity.Proofs.Stdlib.Int256.neg_minValue -- private + Verity.Proofs.Stdlib.Int256.toInt_neg_of_not_min + -- Verity.Proofs.Stdlib.Int256.toNat_lt_signBit_of_nonneg -- private + -- Verity.Proofs.Stdlib.Int256.natAbs_le_signBit_of_inRange -- private + -- Verity.Proofs.Stdlib.Int256.inRange_of_natAbs_lt_signBit -- private + Verity.Proofs.Stdlib.Int256.toInt_ofInt + -- Verity.Proofs.Stdlib.Int256.decide_natCast_lt_zero -- private + -- Verity.Proofs.Stdlib.Int256.tdiv_eq_sign_natAbs -- private + -- Verity.Proofs.Stdlib.Int256.tmod_eq_sign_natAbs -- private + Verity.Proofs.Stdlib.Int256.div_eq_ofInt_tdiv + -- Verity.Proofs.Stdlib.Int256.tdiv_inRange_of_not_divFails -- private + Verity.Proofs.Stdlib.Int256.toInt_div_of_not_divFails + Verity.Proofs.Stdlib.Int256.mod_eq_ofInt_tmod + -- Verity.Proofs.Stdlib.Int256.tmod_inRange -- private + Verity.Proofs.Stdlib.Int256.toInt_mod_of_ne_zero + Verity.Proofs.Stdlib.Int256.addPanic_success_toInt + Verity.Proofs.Stdlib.Int256.subPanic_success_toInt + Verity.Proofs.Stdlib.Int256.mulPanic_success_toInt + Verity.Proofs.Stdlib.Int256.negPanic_success_toInt + Verity.Proofs.Stdlib.Int256.divPanic_success_toInt + Verity.Proofs.Stdlib.Int256.modPanic_success_toInt + Verity.Proofs.Stdlib.Int256.addPanic_failure_iff + Verity.Proofs.Stdlib.Int256.subPanic_failure_iff + Verity.Proofs.Stdlib.Int256.mulPanic_failure_iff + Verity.Proofs.Stdlib.Int256.negPanic_failure_iff + Verity.Proofs.Stdlib.Int256.divPanic_failure_iff + Verity.Proofs.Stdlib.Int256.modPanic_failure_iff + -- Verity/Proofs/Stdlib/ListSum.lean Verity.Proofs.Stdlib.ListSum.countOcc_cons_eq Verity.Proofs.Stdlib.ListSum.countOcc_cons_ne @@ -7536,4 +7581,4 @@ end Verity.AxiomAudit Compiler.Proofs.YulGeneration.YulTransaction.ofIR_args ] --- Total: 6965 theorems/lemmas (4975 public, 1990 private, 0 sorry'd) +-- Total: 7007 theorems/lemmas (4998 public, 2009 private, 0 sorry'd) From 304742ab295b28e3669fcd0363357afd34868b69 Mon Sep 17 00:00:00 2001 From: Thomas Marchand Date: Sat, 19 Sep 2026 11:37:42 +0100 Subject: [PATCH 34/50] scripts/measure.sh: propagate the lake build exit status (Codex P2) --- scripts/measure.sh | 12 +++++++++++- 1 file changed, 11 insertions(+), 1 deletion(-) diff --git a/scripts/measure.sh b/scripts/measure.sh index 3fb2336b4..d3236afe7 100755 --- a/scripts/measure.sh +++ b/scripts/measure.sh @@ -1,8 +1,18 @@ #!/bin/sh +# Time a full `lake build` and propagate its exit status (the previous +# `lake build | tail` form returned tail's status, so a failed build exited 0). set -eu START=$(date +%s) echo "START: $(date -Iseconds)" -lake build 2>&1 | tail -20 +LOG=$(mktemp) +set +e +lake build >"$LOG" 2>&1 +STATUS=$? +set -e +tail -20 "$LOG" +rm -f "$LOG" END=$(date +%s) echo "DURATION: $((END-START))s" echo "END: $(date -Iseconds)" +echo "BUILD_STATUS: $STATUS" +exit "$STATUS" From 917ed93a1f9c9a8c12e485467b8b028f66c6b040 Mon Sep 17 00:00:00 2001 From: Thomas Marchand Date: Sat, 19 Sep 2026 11:45:55 +0100 Subject: [PATCH 35/50] registry: quantify the executable resolver in generated entrypoint predicates (Codex P1) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The entrypoint predicate applied *_registry through ExecutableCallContext.ofAdversary, whose resolver fixes target/value to 0, so transitions produced under ofCallEnv or a custom resolver were not registered and CallbackBounded did not cover them. Each *_entrypoint now quantifies ∃ resolve, and applies the registry executable to { adversary, resolve }. guardedPing_registered is generalized to any ExecutableCallContext (structure eta closes the rfl); the ofAdversary instance is kept. --- TRUST_ASSUMPTIONS.md | 11 ++++++---- Verity/Macro/Translate.lean | 14 ++++++++++++- .../Model/GeneratedEntrypointRegistry.lean | 20 ++++++++++++++----- 3 files changed, 35 insertions(+), 10 deletions(-) diff --git a/TRUST_ASSUMPTIONS.md b/TRUST_ASSUMPTIONS.md index f1ea204b2..10a8dd0db 100644 --- a/TRUST_ASSUMPTIONS.md +++ b/TRUST_ASSUMPTIONS.md @@ -423,10 +423,13 @@ of byte-for-byte EVM ABI layout. Trust boundaries of that plane: about call *outcomes* are therefore claims about the stub, not about a real callee; adversarial reasoning lives in the model plane (`DenoteExternalCalls`). - **`ExecutableCallContext.ofAdversary` pins `target = 0` and `value = 0`.** - The registry predicate applies generated `*_registry` executables through - this helper so the adversary is explicit while link-time callee address and - ETH value stay at the zero boundary. A nonzero target or value requires - `ofCallEnv` or a custom `resolve`; ofAdversary is not a general linker. + It is a convenience context, not a general linker: a nonzero target or value + requires `ofCallEnv` or a custom `resolve`. The generated registry predicate + does not depend on it: each `*_entrypoint` existentially quantifies the + executable resolver, so a transition produced by any `ExecutableCallContext` + carrying the registry adversary (including `ofCallEnv`) is a registered + transition. `CallbackBounded` therefore covers executable linked calls that + resolve a nonzero target/value, not only the zero boundary. - **`externalCallWords` (pure expression form) does not journal.** It is not monadic, so `externalCall name [args]` used as a pure expression remains observationally silent; only the monadic forms journal. Specs that need diff --git a/Verity/Macro/Translate.lean b/Verity/Macro/Translate.lean index 2ef15d7bb..c6c48cc41 100644 --- a/Verity/Macro/Translate.lean +++ b/Verity/Macro/Translate.lean @@ -6339,8 +6339,16 @@ def mkFunctionCommandsPublic let registryAdv : Ident := ⟨registryAdvIdent.raw⟩ let transition : Ident := ⟨transitionIdent.raw⟩ let context : Ident := ⟨contextIdent.raw⟩ + -- The executable resolver is existentially quantified: a registered + -- transition may come from any `ExecutableCallContext` carrying the + -- registry adversary (e.g. `ofCallEnv`), not only from `ofAdversary`, + -- whose resolver fixes target/value to 0 (Codex P1 on #2406). + let resolveIdent ← Lean.Elab.Term.mkFreshIdent + (mkIdentFrom fn.ident `_registryResolve).raw + let resolve : Ident := ⟨resolveIdent.raw⟩ let mut applied : Term := registryId - applied ← `($applied (Contracts.ExecutableCallContext.ofAdversary $registryAdv:ident)) + applied ← `($applied ({ adversary := $registryAdv:ident, resolve := $resolve:ident } : + Contracts.ExecutableCallContext)) let mut registryParams : Array (Ident × Term) := #[] for param in fn.params do let paramTy ← contractValueTypeTerm param.ty @@ -6357,6 +6365,10 @@ def mkFunctionCommandsPublic registryBody ← `(($context:ident).msgValue = 0 ∧ $registryBody) for (paramIdent, paramTy) in registryParams.reverse do registryBody ← `(∃ $paramIdent:ident : $paramTy, $registryBody) + registryBody ← + `(∃ $resolve:ident : + String → Nat → Option Compiler.CompilationModel.DenoteFunctionCalls.LinkedExternal, + $registryBody) registryBody ← `(∃ $context:ident : Compiler.CompilationModel.DenoteExternalCalls.CallbackContext, diff --git a/Verity/Proofs/Model/GeneratedEntrypointRegistry.lean b/Verity/Proofs/Model/GeneratedEntrypointRegistry.lean index 4ca5d2171..913d68d0e 100644 --- a/Verity/Proofs/Model/GeneratedEntrypointRegistry.lean +++ b/Verity/Proofs/Model/GeneratedEntrypointRegistry.lean @@ -28,14 +28,24 @@ verity_contract GeneratedRegistry where namespace GeneratedRegistry /-- The generated registry uses its explicit adversary at the external-call -entrypoint; there is no `.stub` compatibility path in this theorem surface. -/ -theorem guardedPing_registered (adv : AdversaryModel) (ctx : CallbackContext) +entrypoint; there is no `.stub` compatibility path in this theorem surface. +The registry quantifies over the executable resolver, so any +`ExecutableCallContext` carrying the adversary is covered, not only +`ofAdversary` (whose resolver fixes target/value to 0). -/ +theorem guardedPing_registered (ectx : Contracts.ExecutableCallContext) (ctx : CallbackContext) + (value : Uint256) (hvalue : ctx.msgValue = 0) : + entrypointRegistry ectx.adversary + (callbackContractTransition ctx (guardedPing_registry ectx value)) := by + left + exact ⟨ctx, ectx.resolve, value, hvalue, rfl⟩ + +/-- The `ofAdversary` instance of the general registration theorem. -/ +theorem guardedPing_registered_ofAdversary (adv : AdversaryModel) (ctx : CallbackContext) (value : Uint256) (hvalue : ctx.msgValue = 0) : entrypointRegistry adv (callbackContractTransition ctx - (guardedPing_registry (Contracts.ExecutableCallContext.ofAdversary adv) value)) := by - left - exact ⟨ctx, value, hvalue, rfl⟩ + (guardedPing_registry (Contracts.ExecutableCallContext.ofAdversary adv) value)) := + guardedPing_registered (Contracts.ExecutableCallContext.ofAdversary adv) ctx value hvalue /-- The executable generated entrypoint is definitionally protected by the canonical source guard at the same slot used by the compiled dispatch guard. -/ From c2014e4ead5e8def88034df9fb80c9359d535484 Mon Sep 17 00:00:00 2001 From: "github-actions[bot]" Date: Sat, 19 Sep 2026 12:46:45 +0200 Subject: [PATCH 36/50] chore: auto-refresh derived artifacts --- PrintAxioms.lean | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/PrintAxioms.lean b/PrintAxioms.lean index 10382ef40..81cd6a9aa 100644 --- a/PrintAxioms.lean +++ b/PrintAxioms.lean @@ -745,6 +745,7 @@ end Verity.AxiomAudit -- Verity/Proofs/Model/GeneratedEntrypointRegistry.lean Contracts.ReentrancyRelyGuarantee.GeneratedRegistry.guardedPing_registered + Contracts.ReentrancyRelyGuarantee.GeneratedRegistry.guardedPing_registered_ofAdversary Contracts.ReentrancyRelyGuarantee.GeneratedRegistry.guardedPing_reentry_blocked Contracts.ReentrancyRelyGuarantee.generated_registry_callback_preserves @@ -7593,4 +7594,4 @@ end Verity.AxiomAudit Compiler.Proofs.YulGeneration.YulTransaction.ofIR_args ] --- Total: 7016 theorems/lemmas (5007 public, 2009 private, 0 sorry'd) +-- Total: 7017 theorems/lemmas (5008 public, 2009 private, 0 sorry'd) From a66f3c4673eed8c65fc95172aaeb262367445427 Mon Sep 17 00:00:00 2001 From: Thomas Marchand Date: Sat, 19 Sep 2026 17:37:24 +0100 Subject: [PATCH 37/50] merge main (#2430): thread linkedContracts through the registry-aware adversary threading Resolves the Translate.lean conflict between the PR4 signature of threadAdversaryThroughExecutableSyntax (fields/helpers/locals) and #2430's linkedContracts parameter: the extended signature gains the optional linkedContracts argument and every internal and external call passes it. --- Verity/Macro/Translate.lean | 37 ++++++++++--------------------------- 1 file changed, 10 insertions(+), 27 deletions(-) diff --git a/Verity/Macro/Translate.lean b/Verity/Macro/Translate.lean index 2bfcb9b8d..031bb3a1a 100644 --- a/Verity/Macro/Translate.lean +++ b/Verity/Macro/Translate.lean @@ -3059,17 +3059,12 @@ private partial def threadAdversaryThroughExecutableSyntax (adversarialHelpers : Array FunctionDecl) (registryOnlyHelpers : Array FunctionDecl) (params : Array ParamDecl) -<<<<<<< HEAD (locals : Array TypedLocal) - (adv : Term) (stx : Syntax) : CommandElabM Syntax := do - let go := threadAdversaryThroughExecutableSyntax fields constDecls immutableDecls - externalDecls helpers adversarialHelpers registryOnlyHelpers params locals adv -======= (adv : Term) (stx : Syntax) (linkedContracts : Array LinkedContractDecl := #[]) : CommandElabM Syntax := do - let go := threadAdversaryThroughExecutableSyntax externalDecls adversarialHelpers params adv + let go := threadAdversaryThroughExecutableSyntax fields constDecls immutableDecls + externalDecls helpers adversarialHelpers registryOnlyHelpers params locals adv (linkedContracts := linkedContracts) ->>>>>>> origin/main let recurseChildren : CommandElabM Syntax := do match stx with | .node info kind args => @@ -3275,6 +3270,7 @@ private partial def threadAdversaryThroughExecutableSyntax for elem in elems do let raw ← threadAdversaryThroughExecutableSyntax fields constDecls immutableDecls externalDecls helpers adversarialHelpers registryOnlyHelpers params scope adv elem.raw + (linkedContracts := linkedContracts) rewritten := rewritten.push ⟨raw⟩ scope ← extendLocals scope elem `(doSeq| $[$rewritten:doElem]*) @@ -6200,13 +6196,9 @@ def mkConstructorDefCommandPublic pure ⟨advIdent.raw⟩ else `(Compiler.CompilationModel.DenoteExternalCalls.AdversaryModel.stub) -<<<<<<< HEAD let executableBody := ⟨← threadAdversaryThroughExecutableSyntax fields constDecls immutableDecls - externalDecls functions adversarialHelpers #[] ctor.params #[] advTerm executableBody.raw⟩ -======= - let executableBody := ⟨← threadAdversaryThroughExecutableSyntax externalDecls adversarialHelpers - ctor.params advTerm executableBody.raw (linkedContracts := linkedContracts)⟩ ->>>>>>> origin/main + externalDecls functions adversarialHelpers #[] ctor.params #[] advTerm executableBody.raw + (linkedContracts := linkedContracts)⟩ let fnType ← if opensReentrancyWindow then mkContractFnTypeWithAdversary ctor.params .unit else @@ -6276,13 +6268,9 @@ def mkHostConstructorDefCommandPublic preludes := preludes.push (← `(doElem| $tgt:ident $args*)) let body ← `(term| do $[$preludes:doElem]* $[$elems:doElem]*) let executableBody ← rewriteForEachExecutableBody fields externalDecls ctor.params body -<<<<<<< HEAD let executableBody := ⟨← threadAdversaryThroughExecutableSyntax fields constDecls immutableDecls - externalDecls functions ownAdversarialHelpers #[] ctor.params #[] advTerm executableBody.raw⟩ -======= - let executableBody := ⟨← threadAdversaryThroughExecutableSyntax externalDecls ownAdversarialHelpers - ctor.params advTerm executableBody.raw (linkedContracts := linkedContracts)⟩ ->>>>>>> origin/main + externalDecls functions ownAdversarialHelpers #[] ctor.params #[] advTerm executableBody.raw + (linkedContracts := linkedContracts)⟩ let fnValue ← if containsExternalCall then mkContractFnValueWithAdversary advIdent ctor.params executableBody else @@ -6457,9 +6445,9 @@ def mkFunctionCommandsPublic mkContractFnTypeWithAdversary fn.params fn.returnTy else mkContractFnType fn.params fn.returnTy -<<<<<<< HEAD let publicExecutableBody := ⟨← threadAdversaryThroughExecutableSyntax fields constDecls immutableDecls - externalDecls functions windowHelpers #[] fn.params #[] advTerm fnExecutableBody.raw⟩ + externalDecls functions windowHelpers #[] fn.params #[] advTerm fnExecutableBody.raw + (linkedContracts := linkedContracts)⟩ -- Registry executables must route every adversarial helper, including -- window-opening helpers, to `_registry` / `_registry_unguarded`. Restricting -- the suffix to non-window helpers let `entry_registry` call public `hop`, @@ -6467,7 +6455,7 @@ def mkFunctionCommandsPublic -- compiled callee-controlled ECM. let registryExecutableBody := ⟨← threadAdversaryThroughExecutableSyntax fields constDecls immutableDecls externalDecls functions adversarialHelpers adversarialHelpers fn.params #[] - (⟨advIdent.raw⟩ : Term) fnExecutableBody.raw⟩ + (⟨advIdent.raw⟩ : Term) fnExecutableBody.raw (linkedContracts := linkedContracts)⟩ let mut extraExecutableCmds : Array Cmd := #[] if fn.nonReentrantLock.isSome && fn.reentrancyTrusted then let unguardedId ← mkSuffixedIdent fn.ident "_unguarded" @@ -6484,11 +6472,6 @@ def mkFunctionCommandsPublic | throwErrorAt lockIdent s!"unknown nonreentrant lock field '{lockName}'" `(Verity.Core.NonReentrantGuard.guarded $(natTerm lockField.slotNum) $publicExecutableBody) | none => pure publicExecutableBody -======= - let fnExecutableBody := ⟨← threadAdversaryThroughExecutableSyntax externalDecls - adversarialHelpers fn.params advTerm fnExecutableBody.raw - (linkedContracts := linkedContracts)⟩ ->>>>>>> origin/main let fnValue ← if opensReentrancyWindow then mkContractFnValueWithAdversary advIdent fn.params publicExecutableBody else From 0ea31d7b7e769fae363495845515b29a3a4b96e6 Mon Sep 17 00:00:00 2001 From: Thomas Marchand Date: Sat, 19 Sep 2026 20:11:07 +0200 Subject: [PATCH 38/50] fix(macro): thread originalArgsForOverload on second-pass helper rewrite Overloaded helper calls with nested externalCall hoisted temps into threadHelperApp?, so overload resolution fell back and registry bodies used stub adversary data. Pass originalArgsForOverload at both hoistNested and let-bind second-pass sites. Add overloaded nested-call regressions. --- .../IRGeneration/NonReentrantGuardIR.lean | 12 +++-- Contracts/Smoke/SecurityCombos.lean | 4 ++ TRUST_ASSUMPTIONS.md | 4 +- Verity/Macro/Translate.lean | 6 ++- .../Model/GeneratedEntrypointRegistry.lean | 47 +++++++++++++++++++ 5 files changed, 66 insertions(+), 7 deletions(-) diff --git a/Compiler/Proofs/IRGeneration/NonReentrantGuardIR.lean b/Compiler/Proofs/IRGeneration/NonReentrantGuardIR.lean index 2d95c80b1..a3ff24130 100644 --- a/Compiler/Proofs/IRGeneration/NonReentrantGuardIR.lean +++ b/Compiler/Proofs/IRGeneration/NonReentrantGuardIR.lean @@ -85,9 +85,15 @@ theorem execIRStmt_lockRelease (fuel : Nat) (state : IRState) (slot : Nat) have hmod : slot % Compiler.Constants.evmModulus = slot := Nat.mod_eq_of_lt hslot simp [lockReleaseStmt, execIRStmt, evalIRExpr, hmod] -/-- The emitted Yul and source model use the same nonzero lock decision. -/ -theorem guard_decision_agrees (v : Nat) : (v ≠ 0) ↔ v ≠ 0 := by - rfl +/-- Yul `if tload(slot)` sees the slot's transient value, so its nonzero +decision is the source model's lock-held predicate `lock ≠ 0` for every +stored value, not only the binary `{0,1}` acquire/release cycle. -/ +theorem guard_decision_agrees (state : IRState) (slot : Nat) + (hslot : slot < Compiler.Constants.evmModulus) : + evalIRExpr state (.call "tload" [.lit slot]) = + some (state.transientStorage slot) := by + have hmod : slot % Compiler.Constants.evmModulus = slot := Nat.mod_eq_of_lt hslot + simp [evalIRExpr, evalIRCall_tload_singleton, hmod] /-- Acquire-then-release round-trips the lock slot: the transient storage function is extensionally the initial one when the slot started free. -/ diff --git a/Contracts/Smoke/SecurityCombos.lean b/Contracts/Smoke/SecurityCombos.lean index 3a0825179..7ce3f9291 100644 --- a/Contracts/Smoke/SecurityCombos.lean +++ b/Contracts/Smoke/SecurityCombos.lean @@ -255,6 +255,10 @@ verity_contract NonreentrantQualifiedHelperResolution where let y ← trustedEntry(externalCall "echo" [x]) return y + function reentrancy_trusted overloadedNestedExternal (x : Uint256) : Uint256 := do + let y ← overloadedAdversarial(externalCall "echo" [x]) + return y + function overloadedTrustedCaller (x : Uint256) : Unit := do let y ← overloadedTrusted x require (y == x) "wrong trusted overload" diff --git a/TRUST_ASSUMPTIONS.md b/TRUST_ASSUMPTIONS.md index 46e8916dd..bdd4a93cb 100644 --- a/TRUST_ASSUMPTIONS.md +++ b/TRUST_ASSUMPTIONS.md @@ -518,8 +518,8 @@ reentry window is closed at the model level. On the compiled side, prologue/release statements under the IR interpreter: locked entry reverts untouched, free entry acquires the lock and changes nothing else, the spliced release resets it (acquire/release round-trips the transient store), and the -Yul decision `if tload(slot)` (nonzero is true) agrees with the model's `lock ≠ 0` on reachable -binary lock values. `Compiler.Proofs.IRGeneration.SpliceSimulation` now proves the full guarded +Yul decision `if tload(slot)` (nonzero is true) agrees with the model's `lock ≠ 0` for every +stored lock value, not only the binary `{0,1}` acquire/release cycle. `Compiler.Proofs.IRGeneration.SpliceSimulation` now proves the full guarded unit end to end for the loop/switch-free fragment with compiler-emitted exits: the general splice simulation (`execIRStmts_spliced`), both `applyLockReleaseOnExits` branches, and diff --git a/Verity/Macro/Translate.lean b/Verity/Macro/Translate.lean index 031bb3a1a..9babfae05 100644 --- a/Verity/Macro/Translate.lean +++ b/Verity/Macro/Translate.lean @@ -3172,7 +3172,8 @@ private partial def threadAdversaryThroughExecutableSyntax binds := binds ++ inner hoisted := hoisted.push rewritten match ← threadHelperApp? fields constDecls immutableDecls externalDecls - helpers adversarialHelpers registryOnlyHelpers params locals name hoisted adv with + helpers adversarialHelpers registryOnlyHelpers params locals name hoisted adv + (originalArgsForOverload := original) with | some app => pure (binds, app) | none => let mut app : Term := ⟨name.raw⟩ @@ -3336,7 +3337,8 @@ private partial def threadAdversaryThroughExecutableSyntax binds := binds ++ inner hoisted := hoisted.push h match ← threadHelperApp? fields constDecls immutableDecls externalDecls - helpers adversarialHelpers registryOnlyHelpers params locals fn hoisted adv with + helpers adversarialHelpers registryOnlyHelpers params locals fn hoisted adv + (originalArgsForOverload := original) with | some app => wrapBinds binds (← `(doElem| let $name ← $app:term)) | none => recurseChildren | none => recurseChildren diff --git a/Verity/Proofs/Model/GeneratedEntrypointRegistry.lean b/Verity/Proofs/Model/GeneratedEntrypointRegistry.lean index 913d68d0e..1134353af 100644 --- a/Verity/Proofs/Model/GeneratedEntrypointRegistry.lean +++ b/Verity/Proofs/Model/GeneratedEntrypointRegistry.lean @@ -129,6 +129,53 @@ example : entryRegistrySeesAdversary = true := by decide end RegistryWindowHelperRouting +/-! Regression: parenthesized overloaded helper + nested `externalCall`. +Second-pass `threadHelperApp?` must resolve the overload from the original +source arguments (`originalArgsForOverload`), not the hoisted temps. Otherwise +the registry body falls through to the public helper and observes stub +returndata. The parenthesized `let observed ← overloadedHop(externalCall ...)` +form is the let-bind second-pass site; `hoistNested` rewrites the nested +`externalCall` argument of that same application. -/ +verity_contract RegistryOverloadedNestedExternal where + storage + last : Uint256 := slot 0 + linked_externals + external ping(Uint256) -> (Uint256) + + function overloadedHop (_who : Address) : Uint256 := do + return 0 + + function reentrancy_trusted overloadedHop (x : Uint256) : Uint256 := do + let observed := externalCall "ping" [x] + return observed + + function allow_post_interaction_writes reentrancy_trusted entryLet (x : Uint256) : Uint256 := do + let observed ← overloadedHop(externalCall "ping" [x]) + setStorage last observed + return observed + +namespace RegistryOverloadedNestedExternal + +def distinctivePingAdv : AdversaryModel where + stateTransition := fun _ state => state + result := fun site world => + if site.name = "ping" then .success [42] + else AdversaryModel.stub.result site world + gasUsed := fun _ _ => 0 + +def distinctivePingCtx : Contracts.ExecutableCallContext := + Contracts.ExecutableCallContext.ofAdversary distinctivePingAdv + +/-- `entryLet_registry` must call `overloadedHop_registry`, not public `overloadedHop`. -/ +def entryLetRegistrySeesAdversary : Bool := + match (entryLet_registry distinctivePingCtx 0).run Verity.defaultState with + | .success value _ => value == 42 + | _ => false + +example : entryLetRegistrySeesAdversary = true := by decide + +end RegistryOverloadedNestedExternal + /-- `ReentrancyRelyGuarantee` consumes the emitted registry at the restricted callback boundary. Contract-specific preservation obligations remain with authors; this PR establishes only the generated registry/guard connection. -/ From 6e1354d4aac6652c548db7278bd7d1153163aa59 Mon Sep 17 00:00:00 2001 From: Thomas Marchand Date: Sat, 19 Sep 2026 22:21:33 +0000 Subject: [PATCH 39/50] registry: route bound callees through *_registry Registry-mode linked_contracts hops now call the bound callee's generated *_registry executable and thread the current ExecutableCallContext, so view/static callees observe the registry adversary instead of the public stub. Helper fixed-point membership uses functionSignatureKey. Refresh the stale nonreentrant property artifact. --- TRUST_ASSUMPTIONS.md | 9 +- Verity/Macro/Translate.lean | 84 ++++++++++++------- .../Model/GeneratedEntrypointRegistry.lean | 62 ++++++++++++++ ...onreentrantQualifiedHelperResolution.t.sol | 27 ++++-- 4 files changed, 141 insertions(+), 41 deletions(-) diff --git a/TRUST_ASSUMPTIONS.md b/TRUST_ASSUMPTIONS.md index 1d079822d..9ddb1aa5e 100644 --- a/TRUST_ASSUMPTIONS.md +++ b/TRUST_ASSUMPTIONS.md @@ -636,8 +636,13 @@ global invariant `I : ContractState → Prop`. reentry window. Omitting a reachable entrypoint voids the guarantee (analogous to declaring the lock field for `nonreentrant`). The macro-emitted entrypoint registry (`entrypointRegistry` and per-function - `*_registry` / `*_registry_unguarded` executables generated by - `verity_contract`) is the source of this list on the macro path. + `*_registry` / `*_registry_unguarded` executables generated by + `verity_contract`) is the source of this list on the macro path. + Registry-mode `linked_contracts` hops use the bound callee's `*_registry` + executable (and thread the current `ExecutableCallContext`) when that + definition takes the context, including view/static callees whose public + bodies do not open a reentrancy window. Public hopCall bodies keep the + ctx-free public definition. Author-supplied lists remain only for hand-written `ReentrancySpec` consumers. 2. **Adversary-model fidelity** — reentry is modeled as an arbitrary diff --git a/Verity/Macro/Translate.lean b/Verity/Macro/Translate.lean index 9babfae05..612242117 100644 --- a/Verity/Macro/Translate.lean +++ b/Verity/Macro/Translate.lean @@ -2694,18 +2694,27 @@ private def constantTakesExecutableCallContext (n : Name) : CommandElabM Bool := return false private def boundCalleeBodyTerm (binding : LinkedContractDecl) (extName : String) - (args : Array Term) (adv : Term) : CommandElabM Term := do + (args : Array Term) (adv : Term) (registryMode : Bool := false) : CommandElabM Term := do let method := match extName.splitOn "." with | _iface :: m :: _ => m | _ => extName - let fnName := binding.calleeIdent.getId ++ Name.mkSimple method + let publicName := binding.calleeIdent.getId ++ Name.mkSimple method + let registryIdent ← mkSuffixedIdent (mkIdent publicName) "_registry" + let registryName := registryIdent.getId + -- Every generated `*_registry` executable takes `ExecutableCallContext`. + -- Do not gate on env lookup: the callee may live in another namespace, and + -- a missed lookup would silently keep the ctx-free public stub path. + let useRegistry := registryMode + let fnName := if useRegistry then registryName else publicName let fnIdent := mkIdent fnName let mut app : Term ← `(term| $fnIdent) -- Thread the caller's ExecutableCallContext into a bound callee that opens a - -- reentrancy window. Ctx-free callees (ModeledCallee.get/set) stay unchanged - -- so existing hopCall definitional theorems keep holding. - if ← constantTakesExecutableCallContext fnName then + -- reentrancy window, and in registry mode also into `*_registry` executables + -- of view/static callees (those take the context even when the public def + -- does not). Ctx-free public callees stay unchanged so existing hopCall + -- definitional theorems keep holding. + if useRegistry || (← constantTakesExecutableCallContext fnName) then app ← `(term| $app $adv) for arg in args do app ← `(term| $app $arg) @@ -2713,10 +2722,11 @@ private def boundCalleeBodyTerm (binding : LinkedContractDecl) (extName : String private def hopBoundCallTerm? (linked : Array LinkedContractDecl) (target : Term) (targetName : String) - (extName : String) (args : Array Term) (isView : Bool) (adv : Term) : + (extName : String) (args : Array Term) (isView : Bool) (adv : Term) + (registryMode : Bool := false) : CommandElabM (Option Term) := do let some binding := lookupLinkedBinding? linked targetName extName | return none - let body ← boundCalleeBodyTerm binding extName args adv + let body ← boundCalleeBodyTerm binding extName args adv (registryMode := registryMode) if isView then some <$> `(term| _root_.Verity.Contract.hopCallView $target $body) else @@ -2726,7 +2736,8 @@ private def rewriteTypedInterfaceCall? (externalDecls : Array ExternalDecl) (params : Array ParamDecl) (adv : Term) (stx : Term) - (linkedContracts : Array LinkedContractDecl := #[]) : CommandElabM (Option Term) := do + (linkedContracts : Array LinkedContractDecl := #[]) + (registryMode : Bool := false) : CommandElabM (Option Term) := do let some (target, methodName, argTerms) := typedDotCallSyntax? stx | pure none let targetName ← match stripParens target with @@ -2736,7 +2747,8 @@ private def rewriteTypedInterfaceCall? let extName := interfaceExternalName interfaceName methodName let some ext := externalDecls.find? (fun ext => ext.name == extName) | pure none let isView := externalDecls.any (fun candidate => candidate.name == extName && candidate.isView) - match ← hopBoundCallTerm? linkedContracts target targetName extName argTerms isView adv with + match ← hopBoundCallTerm? linkedContracts target targetName extName argTerms isView adv + (registryMode := registryMode) with | some hop => return some hop | none => pure () let siteId := natTerm (linkedExternalSiteId externalDecls extName) @@ -2764,7 +2776,8 @@ private def rewriteTypedInterfaceCall? private def rewriteLinkedCallTerm (externalDecls : Array ExternalDecl) (params : Array ParamDecl) (adv : Term) (stx : Term) - (linkedContracts : Array LinkedContractDecl := #[]) : CommandElabM Term := do + (linkedContracts : Array LinkedContractDecl := #[]) + (registryMode : Bool := false) : CommandElabM Term := do match stx with | `(term| __verityTypedCall $target:term $name:term [ $[$args:term],* ]) => let extName ← expectStringOrIdent name @@ -2775,7 +2788,8 @@ private def rewriteLinkedCallTerm match stripParens target with | `(term| $targetIdent:ident) => pure (toString targetIdent.getId) | _ => pure "" - match ← hopBoundCallTerm? linkedContracts target targetName extName args ext.isView adv with + match ← hopBoundCallTerm? linkedContracts target targetName extName args ext.isView adv + (registryMode := registryMode) with | some hop => return hop | none => pure () let rewritten ← args.zip ext.params |>.mapM fun (arg, ty) => do @@ -2800,7 +2814,8 @@ private def rewriteLinkedCallTerm match stripParens target with | `(term| $targetIdent:ident) => pure (toString targetIdent.getId) | _ => pure "" - match ← hopBoundCallTerm? linkedContracts target targetName extName args ext.isView adv with + match ← hopBoundCallTerm? linkedContracts target targetName extName args ext.isView adv + (registryMode := registryMode) with | some hop => return hop | none => pure () let rewritten ← args.zip ext.params |>.mapM fun (arg, ty) => do @@ -2989,7 +3004,8 @@ private def rewriteLinkedCallTerm | `(term| legacyStringSafeTransferFrom $token:term $fromAddr:term $toAddr:term $amount:term) => `(term| legacyStringSafeTransferFrom $token $fromAddr $toAddr $amount $adv) | other => - match ← rewriteTypedInterfaceCall? externalDecls params adv (linkedContracts := linkedContracts) ⟨other.raw⟩ with + match ← rewriteTypedInterfaceCall? externalDecls params adv + (linkedContracts := linkedContracts) (registryMode := registryMode) ⟨other.raw⟩ with | some rewritten => pure rewritten | none => pure other @@ -3061,17 +3077,19 @@ private partial def threadAdversaryThroughExecutableSyntax (params : Array ParamDecl) (locals : Array TypedLocal) (adv : Term) (stx : Syntax) - (linkedContracts : Array LinkedContractDecl := #[]) : CommandElabM Syntax := do + (linkedContracts : Array LinkedContractDecl := #[]) + (registryMode : Bool := false) : CommandElabM Syntax := do let go := threadAdversaryThroughExecutableSyntax fields constDecls immutableDecls externalDecls helpers adversarialHelpers registryOnlyHelpers params locals adv - (linkedContracts := linkedContracts) + (linkedContracts := linkedContracts) (registryMode := registryMode) let recurseChildren : CommandElabM Syntax := do match stx with | .node info kind args => pure (.node info kind (← args.mapM go)) | _ => pure stx let rewriteTerm (t : Term) : CommandElabM Term := do - rewriteLinkedCallTerm externalDecls params adv t (linkedContracts := linkedContracts) + rewriteLinkedCallTerm externalDecls params adv t + (linkedContracts := linkedContracts) (registryMode := registryMode) let freshExternalIdent (origin : Term) : CommandElabM Ident := Lean.Elab.Term.mkFreshIdent (mkIdentFrom origin.raw (Name.mkSimple "__verity_ext")).raw @@ -3215,7 +3233,8 @@ private partial def threadAdversaryThroughExecutableSyntax if isLiveStateExternalCall rebuilt then bindCall binds rebuilt else - match ← rewriteTypedInterfaceCall? externalDecls params adv (linkedContracts := linkedContracts) rebuilt with + match ← rewriteTypedInterfaceCall? externalDecls params adv + (linkedContracts := linkedContracts) (registryMode := registryMode) rebuilt with | some rewritten => if bindSelf then let tmp ← freshExternalIdent t @@ -3227,7 +3246,8 @@ private partial def threadAdversaryThroughExecutableSyntax if isLiveStateExternalCall t then bindCall #[] t else - match ← rewriteTypedInterfaceCall? externalDecls params adv (linkedContracts := linkedContracts) t with + match ← rewriteTypedInterfaceCall? externalDecls params adv + (linkedContracts := linkedContracts) (registryMode := registryMode) t with | some rewritten => if bindSelf then let tmp ← freshExternalIdent t @@ -3271,7 +3291,7 @@ private partial def threadAdversaryThroughExecutableSyntax for elem in elems do let raw ← threadAdversaryThroughExecutableSyntax fields constDecls immutableDecls externalDecls helpers adversarialHelpers registryOnlyHelpers params scope adv elem.raw - (linkedContracts := linkedContracts) + (linkedContracts := linkedContracts) (registryMode := registryMode) rewritten := rewritten.push ⟨raw⟩ scope ← extendLocals scope elem `(doSeq| $[$rewritten:doElem]*) @@ -3283,7 +3303,7 @@ private partial def threadAdversaryThroughExecutableSyntax binds := binds ++ inner rewrittenArgs := rewrittenArgs.push rewritten let call ← `(term| tryExternalCall $name [ $[$rewrittenArgs],* ]) - let rewritten ← rewriteLinkedCallTerm externalDecls params adv (linkedContracts := linkedContracts) call + let rewritten ← rewriteLinkedCallTerm externalDecls params adv (linkedContracts := linkedContracts) (registryMode := registryMode) call wrapBinds binds (← `(doElem| let $pat:term ← $rewritten:term)) | `(doElem| let $pat:term ← callResult $name:term [ $[$args:term],* ]) => let mut binds : Array (Ident × Term) := #[] @@ -3293,7 +3313,7 @@ private partial def threadAdversaryThroughExecutableSyntax binds := binds ++ inner rewrittenArgs := rewrittenArgs.push rewritten let call ← `(term| callResult $name [ $[$rewrittenArgs],* ]) - let rewritten ← rewriteLinkedCallTerm externalDecls params adv (linkedContracts := linkedContracts) call + let rewritten ← rewriteLinkedCallTerm externalDecls params adv (linkedContracts := linkedContracts) (registryMode := registryMode) call wrapBinds binds (← `(doElem| let $pat:term ← $rewritten:term)) | `(doElem| let $pat:term ← callExternal $name:ident ($[$args:term],*)) => let mut binds : Array (Ident × Term) := #[] @@ -3303,7 +3323,7 @@ private partial def threadAdversaryThroughExecutableSyntax binds := binds ++ inner rewrittenArgs := rewrittenArgs.push rewritten let call ← `(term| callExternal $name ($[$rewrittenArgs],*)) - let rewritten ← rewriteLinkedCallTerm externalDecls params adv (linkedContracts := linkedContracts) call + let rewritten ← rewriteLinkedCallTerm externalDecls params adv (linkedContracts := linkedContracts) (registryMode := registryMode) call wrapBinds binds (← `(doElem| let $pat:term ← $rewritten:term)) | `(doElem| let $pat:term ← balanceOf $token:term $owner:term) => let (tokenBinds, rewrittenToken) ← hoistNested true token @@ -3372,7 +3392,7 @@ private partial def threadAdversaryThroughExecutableSyntax if fnName == "__verityTypedCall" then match stx with | `(doElem| let $_ ← $rhs:term) => - let rewritten ← rewriteLinkedCallTerm externalDecls params adv (linkedContracts := linkedContracts) rhs + let rewritten ← rewriteLinkedCallTerm externalDecls params adv (linkedContracts := linkedContracts) (registryMode := registryMode) rhs `(doElem| let $name ← $rewritten:term) | _ => recurseChildren else if fnName == "tryExternalCall" || fnName == "callResult" || fnName == "callExternal" @@ -3489,7 +3509,7 @@ private partial def threadAdversaryThroughExecutableSyntax if toString fn.getId == "__verityTypedEffect" then match stx with | `(doElem| $rhs:term) => - let rewritten ← rewriteLinkedCallTerm externalDecls params adv (linkedContracts := linkedContracts) rhs + let rewritten ← rewriteLinkedCallTerm externalDecls params adv (linkedContracts := linkedContracts) (registryMode := registryMode) rhs `(doElem| $rewritten:term) | _ => recurseChildren else @@ -3511,13 +3531,13 @@ private partial def threadAdversaryThroughExecutableSyntax | some app => pure app.raw | none => if isLiveStateExternalCall ⟨stx⟩ then - (·.raw) <$> rewriteLinkedCallTerm externalDecls params adv (linkedContracts := linkedContracts) ⟨stx⟩ + (·.raw) <$> rewriteLinkedCallTerm externalDecls params adv (linkedContracts := linkedContracts) (registryMode := registryMode) ⟨stx⟩ else recurseChildren | _ => let asTerm : Term := ⟨stx⟩ if isLiveStateExternalCall asTerm then - (·.raw) <$> rewriteLinkedCallTerm externalDecls params adv (linkedContracts := linkedContracts) asTerm + (·.raw) <$> rewriteLinkedCallTerm externalDecls params adv (linkedContracts := linkedContracts) (registryMode := registryMode) asTerm else recurseChildren @@ -6171,7 +6191,8 @@ private def constructorAdversarialHelpers let mut grew := false for helper in functions do let callsAdversarial ← syntaxCallsAnyHelper names helper.body.raw - if !adversarial.any (fun candidate => candidate.name == helper.name) && + if !adversarial.any (fun candidate => + functionSignatureKey candidate == functionSignatureKey helper) && callsAdversarial then adversarial := adversarial.push helper grew := true @@ -6416,7 +6437,8 @@ def mkFunctionCommandsPublic let mut grew := false for (helper, helperModel) in translatedHelpers do let callsAdversarial ← syntaxCallsAnyHelper adversarialNames helperModel.body.raw - if !adversarialHelpers.any (fun candidate => candidate.name == helper.name) && + if !adversarialHelpers.any (fun candidate => + functionSignatureKey candidate == functionSignatureKey helper) && callsAdversarial then adversarialHelpers := adversarialHelpers.push helper grew := true @@ -6427,7 +6449,8 @@ def mkFunctionCommandsPublic let mut grew := false for (helper, helperModel) in translatedHelpers do let callsWindow ← syntaxCallsAnyHelper windowNames helperModel.body.raw - if !windowHelpers.any (fun candidate => candidate.name == helper.name) && + if !windowHelpers.any (fun candidate => + functionSignatureKey candidate == functionSignatureKey helper) && callsWindow then windowHelpers := windowHelpers.push helper grew := true @@ -6457,7 +6480,8 @@ def mkFunctionCommandsPublic -- compiled callee-controlled ECM. let registryExecutableBody := ⟨← threadAdversaryThroughExecutableSyntax fields constDecls immutableDecls externalDecls functions adversarialHelpers adversarialHelpers fn.params #[] - (⟨advIdent.raw⟩ : Term) fnExecutableBody.raw (linkedContracts := linkedContracts)⟩ + (⟨advIdent.raw⟩ : Term) fnExecutableBody.raw (linkedContracts := linkedContracts) + (registryMode := true)⟩ let mut extraExecutableCmds : Array Cmd := #[] if fn.nonReentrantLock.isSome && fn.reentrancyTrusted then let unguardedId ← mkSuffixedIdent fn.ident "_unguarded" diff --git a/Verity/Proofs/Model/GeneratedEntrypointRegistry.lean b/Verity/Proofs/Model/GeneratedEntrypointRegistry.lean index 1134353af..ecb3f92d8 100644 --- a/Verity/Proofs/Model/GeneratedEntrypointRegistry.lean +++ b/Verity/Proofs/Model/GeneratedEntrypointRegistry.lean @@ -176,6 +176,68 @@ example : entryLetRegistrySeesAdversary = true := by decide end RegistryOverloadedNestedExternal +/-! Regression: `linked_contracts` hop to a view/static-only bound callee. +Public `helper.get` uses `.stub`; `entry_registry` must hop through +`BoundViewCallee.get_registry` so a distinctive staticcall adversary is +observed (Codex P1 on #2406). -/ +verity_contract BoundViewCallee where + storage + unused : Uint256 := slot 0 + interfaces + interface IToken where + function balanceOf(Address) view returns (Uint256) + end + + function view get (token : IToken, who : Address) : Uint256 := do + let observed ← token.balanceOf who + return observed + +verity_contract BoundViewCaller where + storage + last : Uint256 := slot 0 + interfaces + interface IViewCallee where + function get(Address, Address) view returns (Uint256) + end + linked_contracts + helper : IViewCallee := BoundViewCallee + + function allow_post_interaction_writes reentrancy_trusted entry + (helper : IViewCallee, token : Address, who : Address) : Uint256 := do + let observed ← helper.get token who + setStorage last observed + return observed + +namespace BoundViewCaller + +def distinctiveViewAdv : AdversaryModel where + stateTransition := fun _ state => state + result := fun site world => + if site.kind = .staticcall then .success [42] + else AdversaryModel.stub.result site world + gasUsed := fun _ _ => 0 + +def distinctiveCtx : Contracts.ExecutableCallContext := + Contracts.ExecutableCallContext.ofAdversary distinctiveViewAdv + +/-- Public `entry` is ctx-free and hops to stub-backed `BoundViewCallee.get`. -/ +def entryPublicSeesStub : Bool := + match (entry 0 0 0).run Verity.defaultState with + | .success value _ => !(value == 42) + | _ => false + +example : entryPublicSeesStub = true := by decide + +/-- `entry_registry` hops through `BoundViewCallee.get_registry`. -/ +def entryRegistrySeesAdversary : Bool := + match (entry_registry distinctiveCtx 0 0 0).run Verity.defaultState with + | .success value _ => value == 42 + | _ => false + +example : entryRegistrySeesAdversary = true := by decide + +end BoundViewCaller + /-- `ReentrancyRelyGuarantee` consumes the emitted registry at the restricted callback boundary. Contract-specific preservation obligations remain with authors; this PR establishes only the generated registry/guard connection. -/ diff --git a/artifacts/macro_property_tests/PropertyNonreentrantQualifiedHelperResolution.t.sol b/artifacts/macro_property_tests/PropertyNonreentrantQualifiedHelperResolution.t.sol index 9a290db10..f382a5ab7 100644 --- a/artifacts/macro_property_tests/PropertyNonreentrantQualifiedHelperResolution.t.sol +++ b/artifacts/macro_property_tests/PropertyNonreentrantQualifiedHelperResolution.t.sol @@ -154,55 +154,64 @@ contract PropertyNonreentrantQualifiedHelperResolutionTest is YulTestBase { // TODO(#1011): decode `ret` and assert the concrete postcondition from Lean theorem. ret; } - // Property 16: overloadedTrustedCaller has no unexpected revert + // Property 16: TODO decode and assert `overloadedNestedExternal` result + function testTODO_OverloadedNestedExternal_DecodeAndAssert() public { + vm.prank(alice); + (bool ok, bytes memory ret) = target.call(abi.encodeWithSignature("overloadedNestedExternal(uint256)", uint256(1))); + require(ok, "overloadedNestedExternal reverted unexpectedly"); + assertEq(ret.length, 32, "overloadedNestedExternal ABI return length mismatch (expected 32 bytes)"); + // TODO(#1011): decode `ret` and assert the concrete postcondition from Lean theorem. + ret; + } + // Property 17: overloadedTrustedCaller has no unexpected revert function testAuto_OverloadedTrustedCaller_NoUnexpectedRevert() public { vm.prank(alice); (bool ok,) = target.call(abi.encodeWithSignature("overloadedTrustedCaller(uint256)", uint256(1))); require(ok, "overloadedTrustedCaller reverted unexpectedly"); } - // Property 17: overloadedAdversarialCaller has no unexpected revert + // Property 18: overloadedAdversarialCaller has no unexpected revert function testAuto_OverloadedAdversarialCaller_NoUnexpectedRevert() public { vm.prank(alice); (bool ok,) = target.call(abi.encodeWithSignature("overloadedAdversarialCaller(uint256)", uint256(1))); require(ok, "overloadedAdversarialCaller reverted unexpectedly"); } - // Property 18: overloadedTrustedLocalCaller has no unexpected revert + // Property 19: overloadedTrustedLocalCaller has no unexpected revert function testAuto_OverloadedTrustedLocalCaller_NoUnexpectedRevert() public { vm.prank(alice); (bool ok,) = target.call(abi.encodeWithSignature("overloadedTrustedLocalCaller()")); require(ok, "overloadedTrustedLocalCaller reverted unexpectedly"); } - // Property 19: overloadedAdversarialLocalCaller has no unexpected revert + // Property 20: overloadedAdversarialLocalCaller has no unexpected revert function testAuto_OverloadedAdversarialLocalCaller_NoUnexpectedRevert() public { vm.prank(alice); (bool ok,) = target.call(abi.encodeWithSignature("overloadedAdversarialLocalCaller()")); require(ok, "overloadedAdversarialLocalCaller reverted unexpectedly"); } - // Property 20: overloadedTrustedTupleLocalCaller has no unexpected revert + // Property 21: overloadedTrustedTupleLocalCaller has no unexpected revert function testAuto_OverloadedTrustedTupleLocalCaller_NoUnexpectedRevert() public { vm.prank(alice); (bool ok,) = target.call(abi.encodeWithSignature("overloadedTrustedTupleLocalCaller(uint256)", uint256(1))); require(ok, "overloadedTrustedTupleLocalCaller reverted unexpectedly"); } - // Property 21: overloadedAdversarialTupleLocalCaller has no unexpected revert + // Property 22: overloadedAdversarialTupleLocalCaller has no unexpected revert function testAuto_OverloadedAdversarialTupleLocalCaller_NoUnexpectedRevert() public { vm.prank(alice); (bool ok,) = target.call(abi.encodeWithSignature("overloadedAdversarialTupleLocalCaller(uint256)", uint256(1))); require(ok, "overloadedAdversarialTupleLocalCaller reverted unexpectedly"); } - // Property 22: overloadedTrustedQualifiedTupleCaller has no unexpected revert + // Property 23: overloadedTrustedQualifiedTupleCaller has no unexpected revert function testAuto_OverloadedTrustedQualifiedTupleCaller_NoUnexpectedRevert() public { vm.prank(alice); (bool ok,) = target.call(abi.encodeWithSignature("overloadedTrustedQualifiedTupleCaller(uint256)", uint256(1))); require(ok, "overloadedTrustedQualifiedTupleCaller reverted unexpectedly"); } - // Property 23: overloadedTrustedForEachCaller has no unexpected revert + // Property 24: overloadedTrustedForEachCaller has no unexpected revert function testAuto_OverloadedTrustedForEachCaller_NoUnexpectedRevert() public { vm.prank(alice); (bool ok,) = target.call(abi.encodeWithSignature("overloadedTrustedForEachCaller()")); require(ok, "overloadedTrustedForEachCaller reverted unexpectedly"); } - // Property 24: overloadedAdversarialForEachSetBitCaller has no unexpected revert + // Property 25: overloadedAdversarialForEachSetBitCaller has no unexpected revert function testAuto_OverloadedAdversarialForEachSetBitCaller_NoUnexpectedRevert() public { vm.prank(alice); (bool ok,) = target.call(abi.encodeWithSignature("overloadedAdversarialForEachSetBitCaller()")); From 4316953d2934760c33b6385f4cd0c5529cb5709a Mon Sep 17 00:00:00 2001 From: Thomas Marchand Date: Sun, 20 Sep 2026 04:59:54 +0200 Subject: [PATCH 40/50] registry: bind entrypoint args to dispatch calldata; keep selfCall guarded Tighten generated *_entrypoint predicates so Lean arguments must ABI-decode from the same CallbackContext.calldata compiled dispatch uses, and receive is registered only for empty calldata. Make selfCallCallee? monadic so syntax quotations type-check and hops keep the public/registry guard path. --- TRUST_ASSUMPTIONS.md | 6 +- Verity/Core/Model/CallbackBridge.lean | 28 +++- Verity/Macro/Translate.lean | 128 ++++++++++++++- .../Model/GeneratedEntrypointRegistry.lean | 154 +++++++++++++++++- 4 files changed, 305 insertions(+), 11 deletions(-) diff --git a/TRUST_ASSUMPTIONS.md b/TRUST_ASSUMPTIONS.md index 9ddb1aa5e..4119beae1 100644 --- a/TRUST_ASSUMPTIONS.md +++ b/TRUST_ASSUMPTIONS.md @@ -642,7 +642,11 @@ global invariant `I : ContractState → Prop`. executable (and thread the current `ExecutableCallContext`) when that definition takes the context, including view/static callees whose public bodies do not open a reentrancy window. Public hopCall bodies keep the - ctx-free public definition. + ctx-free public definition. Generated `*_entrypoint` predicates require + Lean arguments to ABI-decode from the same `CallbackContext.calldata` + (`dispatchCalldataMatches`); `receive` is registered only for empty + calldata. Same-contract `selfCall` hops keep the guarded public / + `*_registry` path so they still observe the nonReentrant tload prologue. Author-supplied lists remain only for hand-written `ReentrancySpec` consumers. 2. **Adversary-model fidelity** — reentry is modeled as an arbitrary diff --git a/Verity/Core/Model/CallbackBridge.lean b/Verity/Core/Model/CallbackBridge.lean index b83306ae3..df2dff56f 100644 --- a/Verity/Core/Model/CallbackBridge.lean +++ b/Verity/Core/Model/CallbackBridge.lean @@ -23,9 +23,9 @@ open Verity.Core.Invariant (Preserves runSeq) open Verity.Core.Reentrancy (ReentrancySpec) /-- The macro-emitted registry is a predicate rather than a list of already -applied functions. This keeps entrypoint arguments existential and, crucially, -indexes every executable transition by the same explicit adversary used at the -call boundary. -/ +applied functions. Entrypoint arguments stay existential, but they are +tied to the same calldata the compiled dispatcher ABI-decodes, and every +transition is indexed by the explicit adversary used at the call boundary. -/ abbrev EntrypointRegistry := AdversaryModel → (Verity.ContractState → Verity.ContractState) → Prop @@ -52,6 +52,28 @@ structure CallbackContext where calldataSize : Verity.Uint256 calldata : List Nat +/-- Compiled dispatch ABI-decodes arguments from the same calldata that +selected the function (`calldataload` at 4 + 32*i, `calldatasize` at +least 4 + 32 * n). Extra trailing words are allowed, matching Yul +`calldatasizeGuard`. -/ +def dispatchCalldataMatches (ctx : CallbackContext) (argWords : List Nat) : Prop := + ctx.calldata.take argWords.length = argWords ∧ + Verity.Core.Uint256.ofNat (4 + 32 * argWords.length) ≤ ctx.calldataSize + +instance (ctx : CallbackContext) (argWords : List Nat) : + Decidable (dispatchCalldataMatches ctx argWords) := by + dsimp [dispatchCalldataMatches] + infer_instance + +/-- Compiled `receive()` runs only when `calldatasize == 0`. -/ +def receiveCalldataMatches (ctx : CallbackContext) : Prop := + ctx.calldata = [] ∧ ctx.calldataSize = 0 + +instance (ctx : CallbackContext) : + Decidable (receiveCalldataMatches ctx) := by + dsimp [receiveCalldataMatches] + infer_instance + /-- Execute a registered callback in its own call frame, then restore the outer frame's ambient context while retaining the callback's contract-state effects. -/ diff --git a/Verity/Macro/Translate.lean b/Verity/Macro/Translate.lean index 612242117..ab6ae1575 100644 --- a/Verity/Macro/Translate.lean +++ b/Verity/Macro/Translate.lean @@ -2578,6 +2578,56 @@ private def helperCall (name : Ident) (args : Array Term) : CommandElabM Term := app ← `(term| $app $arg) pure app +private def identEndsWithSuffix (name : Name) (suffix : String) : Bool := + let s := toString name + s == suffix || s.endsWith ("." ++ suffix) + +private partial def syntaxEndsWithSuffix (stx : Syntax) (suffix : String) : Bool := + match stx with + | .ident _ _ name _ => identEndsWithSuffix name suffix + | .node _ kind args => + if kind == ``Lean.Parser.Term.proj && args.size >= 3 then + syntaxEndsWithSuffix args[2]! suffix + else if args.isEmpty then + false + else + syntaxEndsWithSuffix args.back! suffix + | _ => false + +private def selfCallCallee? (t : Term) : CommandElabM (Option (Ident × Array Term)) := do + let calleeFrom (inner : Term) : CommandElabM (Option (Ident × Array Term)) := do + match stripParens inner with + | `(term| $fn:ident) => pure (some (fn, #[])) + | `(term| $fn:ident()) => pure (some (fn, #[])) + | `(term| $fn:ident($[$args:term],*)) => pure (some (fn, args)) + | `(term| $fn:ident $args:term*) => pure (some (fn, args)) + | _ => pure none + match t with + | `(term| selfCall $fn:ident) => pure (some (fn, #[])) + | `(term| selfCall $fn:ident($[$args:term],*)) => pure (some (fn, args)) + | `(term| _root_.Verity.Contract.selfCall $inner:term) => calleeFrom inner + | `(term| Verity.Contract.selfCall $inner:term) => calleeFrom inner + | `(term| Contract.selfCall $inner:term) => calleeFrom inner + | `(term| $name:ident $inner:term) => + if identEndsWithSuffix name.getId "selfCall" then + calleeFrom inner + else + pure none + | `(term| $name:ident($[$args:term],*)) => + if identEndsWithSuffix name.getId "selfCall" && args.size == 1 then + calleeFrom ⟨args[0]!.raw⟩ + else + pure none + | _ => + match t.raw with + | .node _ kind args => + if kind == ``Lean.Parser.Term.app && args.size >= 2 && + syntaxEndsWithSuffix args[0]! "selfCall" then + calleeFrom ⟨args[1]!⟩ + else + pure none + | _ => pure none + private def threadHelperApp? (fields : Array StorageFieldDecl) (constDecls : Array ConstantDecl) (immutableDecls : Array ImmutableDecl) @@ -2587,7 +2637,8 @@ private def threadHelperApp? (params : Array ParamDecl) (locals : Array TypedLocal) (name : Ident) (args : Array Term) (adv : Term) - (originalArgsForOverload : Option (Array Term) := none) : CommandElabM (Option Term) := do + (originalArgsForOverload : Option (Array Term) := none) + (keepPublicGuard : Bool := false) : CommandElabM (Option Term) := do let matchesHelper := fun (fn : FunctionDecl) => (fn.name == toString name.getId || fn.ident.getId == name.getId || (toString name.getId).endsWith ("." ++ fn.name)) && @@ -2621,10 +2672,18 @@ private def threadHelperApp? -- / `_registry_unguarded`. Public bodies pass `#[]`; registry bodies pass -- every adversarial helper, including window-opening ones, so a nested -- `hop` cannot drop into the stub-only public helper. + -- Same-contract `selfCall` is a public CALL: compiled dispatch still + -- runs the nonReentrant tload prologue, so hops keep the guarded + -- `*_registry` / public path rather than `*_unguarded`. let registryOnly := registryOnlyHelpers.any (fun candidate => functionSignatureKey candidate == functionSignatureKey helper) let target ← - if registryOnly && helper.nonReentrantLock.isSome && helper.reentrancyTrusted then + if keepPublicGuard then + if registryOnly then + mkSuffixedIdent helper.ident "_registry" + else + pure helper.ident + else if registryOnly && helper.nonReentrantLock.isSome && helper.reentrancyTrusted then mkSuffixedIdent helper.ident "_registry_unguarded" else if registryOnly then mkSuffixedIdent helper.ident "_registry" @@ -3165,6 +3224,26 @@ private partial def threadAdversaryThroughExecutableSyntax for (tmp, call) in binds.reverse do body ← `(term| _root_.Verity.bind $call (fun $tmp => $body)) pure body + if let some (fn, args) := (← selfCallCallee? t) then + let original := args + let mut binds : Array (Ident × Term) := #[] + let mut hoisted : Array Term := #[] + for arg in args do + let (inner, rewritten) ← hoistNested true arg + binds := binds ++ inner + hoisted := hoisted.push rewritten + match ← threadHelperApp? fields constDecls immutableDecls externalDecls + helpers adversarialHelpers registryOnlyHelpers params locals fn hoisted adv + (originalArgsForOverload := original) (keepPublicGuard := true) with + | some app => + return (binds, ← `(term| _root_.Verity.Contract.selfCall $app)) + | none => + let inner ← + if hoisted.isEmpty then + `(term| $fn:ident()) + else + helperCall fn hoisted + return (binds, ← `(term| _root_.Verity.Contract.selfCall $inner)) match t with | `(term| fun $name:ident => $body:term) => let bodyRaw : Syntax ← go body.raw @@ -3284,6 +3363,26 @@ private partial def threadAdversaryThroughExecutableSyntax | stripped => stripped let rest ← if outerWasBound then pureBinding pureValue else monadic rewritten wrapBinds binds rest + if let some (fn, args) := (← selfCallCallee? ⟨stx⟩) then + let original := args + let mut binds : Array (Ident × Term) := #[] + let mut hoisted : Array Term := #[] + for arg in args do + let (inner, rewritten) ← hoistNested true arg + binds := binds ++ inner + hoisted := hoisted.push rewritten + match ← threadHelperApp? fields constDecls immutableDecls externalDecls + helpers adversarialHelpers registryOnlyHelpers params locals fn hoisted adv + (originalArgsForOverload := original) (keepPublicGuard := true) with + | some app => + return (← wrapBinds binds (← `(doElem| _root_.Verity.Contract.selfCall $app))).raw + | none => + let inner ← + if hoisted.isEmpty then + `(term| $fn:ident()) + else + helperCall fn hoisted + return (← wrapBinds binds (← `(doElem| _root_.Verity.Contract.selfCall $inner))).raw match stx with | `(doSeq| $[$elems:doElem]*) => let mut scope := locals @@ -6576,6 +6675,26 @@ def mkFunctionCommandsPublic $context:ident $applied) if !fn.isPayable then registryBody ← `(($context:ident).msgValue = 0 ∧ $registryBody) + -- Tie Lean arguments to the same calldata the compiled dispatcher + -- ABI-decodes (`calldatasizeGuard` + `calldataload`). `receive` is + -- compiled only when `calldatasize == 0`. + if fn.name == "receive" then + registryBody ← + `(Compiler.CompilationModel.DenoteExternalCalls.receiveCalldataMatches + $context:ident ∧ $registryBody) + else if fn.name != "fallback" then + let mut argWordTerms : Array Term := #[] + for (paramIdent, _) in registryParams do + let words ← `(List.map (fun w => (w : Nat)) (Contracts.ExternalArg.toWords $paramIdent:ident)) + argWordTerms := argWordTerms.push words + let argWordsTerm ← + if argWordTerms.isEmpty then + `( ([] : List Nat) ) + else + `(List.flatten ([ $[$argWordTerms],* ] : List (List Nat))) + registryBody ← + `(Compiler.CompilationModel.DenoteExternalCalls.dispatchCalldataMatches + $context:ident $argWordsTerm ∧ $registryBody) for (paramIdent, paramTy) in registryParams.reverse do registryBody ← `(∃ $paramIdent:ident : $paramTy, $registryBody) registryBody ← @@ -6621,8 +6740,9 @@ def mkFunctionCommandsPublic pure (extraExecutableCmds ++ #[fnCmd, entrypointCmd, bodyCmd, modelCmd]) /-- Emit the contract-wide union of all externally callable entrypoint -predicates. Each per-function predicate keeps arguments existential and uses -the registry's explicit adversary when the function opens a reentrancy window. -/ +predicates. Each per-function predicate keeps arguments existential, ties +them to the same calldata compiled dispatch ABI-decodes, and uses the +registry's explicit adversary when the function opens a reentrancy window. -/ def mkEntrypointRegistryCommandPublic (functions : Array FunctionDecl) : CommandElabM Cmd := do let advIdent ← Lean.Elab.Term.mkFreshIdent (mkIdent `_registryAdv).raw let transitionIdent ← Lean.Elab.Term.mkFreshIdent (mkIdent `_transition).raw diff --git a/Verity/Proofs/Model/GeneratedEntrypointRegistry.lean b/Verity/Proofs/Model/GeneratedEntrypointRegistry.lean index ecb3f92d8..b73ae7e2c 100644 --- a/Verity/Proofs/Model/GeneratedEntrypointRegistry.lean +++ b/Verity/Proofs/Model/GeneratedEntrypointRegistry.lean @@ -6,6 +6,7 @@ namespace Contracts.ReentrancyRelyGuarantee open Contracts open Verity hiding pure bind +open Verity.EVM.Uint256 open Compiler.CompilationModel.DenoteExternalCalls /-! Focused generated consumer for the registry/guard boundary. It contains @@ -33,19 +34,24 @@ The registry quantifies over the executable resolver, so any `ExecutableCallContext` carrying the adversary is covered, not only `ofAdversary` (whose resolver fixes target/value to 0). -/ theorem guardedPing_registered (ectx : Contracts.ExecutableCallContext) (ctx : CallbackContext) - (value : Uint256) (hvalue : ctx.msgValue = 0) : + (value : Uint256) (hvalue : ctx.msgValue = 0) + (hcalldata : dispatchCalldataMatches ctx + (List.map (fun w => (w : Nat)) (Contracts.ExternalArg.toWords value))) : entrypointRegistry ectx.adversary (callbackContractTransition ctx (guardedPing_registry ectx value)) := by left - exact ⟨ctx, ectx.resolve, value, hvalue, rfl⟩ + exact ⟨ctx, ectx.resolve, value, hcalldata, hvalue, rfl⟩ /-- The `ofAdversary` instance of the general registration theorem. -/ theorem guardedPing_registered_ofAdversary (adv : AdversaryModel) (ctx : CallbackContext) - (value : Uint256) (hvalue : ctx.msgValue = 0) : + (value : Uint256) (hvalue : ctx.msgValue = 0) + (hcalldata : dispatchCalldataMatches ctx + (List.map (fun w => (w : Nat)) (Contracts.ExternalArg.toWords value))) : entrypointRegistry adv (callbackContractTransition ctx (guardedPing_registry (Contracts.ExecutableCallContext.ofAdversary adv) value)) := guardedPing_registered (Contracts.ExecutableCallContext.ofAdversary adv) ctx value hvalue + hcalldata /-- The executable generated entrypoint is definitionally protected by the canonical source guard at the same slot used by the compiled dispatch guard. -/ @@ -238,6 +244,148 @@ example : entryRegistrySeesAdversary = true := by decide end BoundViewCaller +/-! Regression: registered transitions cannot pair Lean arguments with +unrelated calldata, and `receive` is only registered for empty calldata. -/ +verity_contract RegistryDispatchCalldata where + storage + last : Uint256 := slot 0 + + receive := do + setStorage last 7 + + function setLast (value : Uint256) : Unit := do + setStorage last value + return () + +namespace RegistryDispatchCalldata + +def matchingCtx (value : Uint256) : CallbackContext where + sender := 0 + msgValue := 0 + calldataSize := Verity.Core.Uint256.ofNat 36 + calldata := [value.val] + +def mismatchedCtx : CallbackContext where + sender := 0 + msgValue := 0 + calldataSize := Verity.Core.Uint256.ofNat 36 + calldata := [99] + +def emptyReceiveCtx : CallbackContext where + sender := 0 + msgValue := 0 + calldataSize := 0 + calldata := [] + +def nonemptyReceiveCtx : CallbackContext where + sender := 0 + msgValue := 0 + calldataSize := Verity.Core.Uint256.ofNat 4 + calldata := [1] + +def argWords (value : Uint256) : List Nat := + List.map (fun w => (w : Nat)) (Contracts.ExternalArg.toWords value) + +example : dispatchCalldataMatches (matchingCtx 7) (argWords 7) := by decide + +example : ¬ dispatchCalldataMatches mismatchedCtx (argWords 7) := by decide + +example : receiveCalldataMatches emptyReceiveCtx := by decide + +example : ¬ receiveCalldataMatches nonemptyReceiveCtx := by decide + +theorem setLast_registered_matching (value : Uint256) + (h : dispatchCalldataMatches (matchingCtx value) (argWords value)) : + setLast_entrypoint AdversaryModel.stub + (callbackContractTransition (matchingCtx value) + (setLast_registry (Contracts.ExecutableCallContext.ofAdversary AdversaryModel.stub) value)) := + ⟨matchingCtx value, (Contracts.ExecutableCallContext.ofAdversary AdversaryModel.stub).resolve, + value, h, rfl, rfl⟩ + +theorem setLast_entrypoint_requires_dispatch + {adv : AdversaryModel} {transition : ContractState → ContractState} + (h : setLast_entrypoint adv transition) : + ∃ ctx resolve value, + dispatchCalldataMatches ctx (argWords value) ∧ + ctx.msgValue = 0 ∧ + transition = + callbackContractTransition ctx + (setLast_registry { adversary := adv, resolve := resolve } value) := + h + +theorem receive_registered_empty : + __verity_receive_entrypoint AdversaryModel.stub + (callbackContractTransition emptyReceiveCtx + (__verity_receive_registry + (Contracts.ExecutableCallContext.ofAdversary AdversaryModel.stub))) := + ⟨emptyReceiveCtx, (Contracts.ExecutableCallContext.ofAdversary AdversaryModel.stub).resolve, + by decide, rfl⟩ + +theorem receive_entrypoint_requires_empty + {adv : AdversaryModel} {transition : ContractState → ContractState} + (h : __verity_receive_entrypoint adv transition) : + ∃ ctx resolve, + receiveCalldataMatches ctx ∧ + transition = + callbackContractTransition ctx + (__verity_receive_registry { adversary := adv, resolve := resolve }) := + h + +end RegistryDispatchCalldata + +/-! Regression: public Solidity self-calls still hit the nonReentrant tload +prologue. Bound hops must keep the guarded registry path, not `*_unguarded`. -/ +verity_contract RegistrySelfCallGuard where + storage + lock : Uint256 := slot 0 + last : Uint256 := slot 1 + linked_externals + external ping(Uint256) -> (Uint256) + + function nonreentrant(lock) reentrancy_trusted hop (value : Uint256) : Unit := do + let _ack := externalCall "ping" [value] + setStorage last value + return () + + function reentrancy_trusted allow_post_interaction_writes entry + (value : Uint256) + local_obligations [manual_low_level_refinement := assumed + "tryCall/selfCall compilation model is CALL-with-status to this; selector and argument encoding are a documented gap."] + : Unit := do + tryCall (selfCall hop(value)) then + (do setStorage last value) + catch + (do setStorage last 99) + return () + +namespace RegistrySelfCallGuard + +def lockedState : ContractState := + Verity.defaultState.writeTransient 0 1 + +def hopBlockedWhenLocked : Bool := + match (hop (Contracts.ExecutableCallContext.ofAdversary AdversaryModel.stub) (7 : Uint256)).run lockedState with + | .revert _ s => s.storage 1 == 0 + | _ => false + +example : hopBlockedWhenLocked = true := by decide + +def selfCallHopBlockedWhenLocked : Bool := + match (entry (Contracts.ExecutableCallContext.ofAdversary AdversaryModel.stub) (7 : Uint256)).run lockedState with + | .success _ s => s.storage 1 == 99 + | _ => false + +example : selfCallHopBlockedWhenLocked = true := by decide + +def selfCallHopRegistryBlockedWhenLocked : Bool := + match (entry_registry (Contracts.ExecutableCallContext.ofAdversary AdversaryModel.stub) (7 : Uint256)).run lockedState with + | .success _ s => s.storage 1 == 99 + | _ => false + +example : selfCallHopRegistryBlockedWhenLocked = true := by decide + +end RegistrySelfCallGuard + /-- `ReentrancyRelyGuarantee` consumes the emitted registry at the restricted callback boundary. Contract-specific preservation obligations remain with authors; this PR establishes only the generated registry/guard connection. -/ From 0c445316be92f00079fcd4e81e7f63f949524b07 Mon Sep 17 00:00:00 2001 From: "github-actions[bot]" Date: Sun, 20 Sep 2026 05:00:51 +0200 Subject: [PATCH 41/50] chore: auto-refresh derived artifacts --- PrintAxioms.lean | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/PrintAxioms.lean b/PrintAxioms.lean index 8e52009f9..ea897d0bb 100644 --- a/PrintAxioms.lean +++ b/PrintAxioms.lean @@ -749,6 +749,10 @@ end Verity.AxiomAudit Contracts.ReentrancyRelyGuarantee.GeneratedRegistry.guardedPing_registered Contracts.ReentrancyRelyGuarantee.GeneratedRegistry.guardedPing_registered_ofAdversary Contracts.ReentrancyRelyGuarantee.GeneratedRegistry.guardedPing_reentry_blocked + Contracts.ReentrancyRelyGuarantee.RegistryDispatchCalldata.setLast_registered_matching + Contracts.ReentrancyRelyGuarantee.RegistryDispatchCalldata.setLast_entrypoint_requires_dispatch + Contracts.ReentrancyRelyGuarantee.RegistryDispatchCalldata.receive_registered_empty + Contracts.ReentrancyRelyGuarantee.RegistryDispatchCalldata.receive_entrypoint_requires_empty Contracts.ReentrancyRelyGuarantee.generated_registry_callback_preserves -- Verity/Proofs/Stdlib/Automation.lean @@ -7616,4 +7620,4 @@ end Verity.AxiomAudit Compiler.Proofs.YulGeneration.YulTransaction.ofIR_args ] --- Total: 7033 theorems/lemmas (5020 public, 2013 private, 0 sorry'd) +-- Total: 7037 theorems/lemmas (5024 public, 2013 private, 0 sorry'd) From ef0321d654adc87b62f1fc4135c3611fc0e0829f Mon Sep 17 00:00:00 2001 From: Thomas Marchand Date: Sun, 20 Sep 2026 08:38:03 +0200 Subject: [PATCH 42/50] fix(ci): run Verify proofs after PrintAxioms auto-refresh The bot auto-refresh of PrintAxioms.lean did not match the Verify proofs path filter, so run 35485473453 completed as action_required with zero jobs. Include PrintAxioms.lean in the check-only path filter. Regenerate artifacts/trust_surface_report.json with the other derived artifacts so make check cannot fail on a stale trust-surface report after auto-refresh (the failure on 4316953d). --- .github/workflows/verify.yml | 3 +++ AUDIT.md | 2 +- artifacts/trust_surface_report.json | 2 +- scripts/refresh_verification_artifacts.sh | 2 ++ scripts/verify_sync_spec.json | 2 ++ scripts/verify_sync_spec_source.py | 6 ++++-- 6 files changed, 13 insertions(+), 4 deletions(-) diff --git a/.github/workflows/verify.yml b/.github/workflows/verify.yml index bbf17e98e..45f6370b8 100644 --- a/.github/workflows/verify.yml +++ b/.github/workflows/verify.yml @@ -29,6 +29,7 @@ on: - 'foundry.toml' - 'Makefile' - 'AXIOMS.md' + - 'PrintAxioms.lean' - 'README.md' - 'TRUST_ASSUMPTIONS.md' pull_request: @@ -57,6 +58,7 @@ on: - 'foundry.toml' - 'Makefile' - 'AXIOMS.md' + - 'PrintAxioms.lean' - 'README.md' - 'TRUST_ASSUMPTIONS.md' workflow_dispatch: @@ -310,6 +312,7 @@ jobs: python3 scripts/generate_evmyullean_capability_report.py python3 scripts/generate_evmyullean_native_lowering_report.py python3 scripts/generate_print_axioms.py + python3 scripts/generate_trust_surface_report.py python3 scripts/sync_verification_status_doc.py - name: Auto-commit refreshed artifacts diff --git a/AUDIT.md b/AUDIT.md index 9b80420f4..b8a644de9 100644 --- a/AUDIT.md +++ b/AUDIT.md @@ -620,7 +620,7 @@ sibling entrypoint. | `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` | +| `PrintAxioms.lean` / generated axiom report | Axiom dependency visibility | `python3 scripts/generate_print_axioms.py --check` and `lake build PrintAxioms`. `PrintAxioms.lean` is a Verify proofs path-filter so bot auto-refresh of that file still runs `checks`. The checks job regenerates `artifacts/trust_surface_report.json` with the other derived artifacts so `make check` cannot fail on a stale trust-surface report after auto-refresh. | | `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` | diff --git a/artifacts/trust_surface_report.json b/artifacts/trust_surface_report.json index f8ce7b16b..b390cc7bf 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": 178 + "partial def": 179 }, "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/refresh_verification_artifacts.sh b/scripts/refresh_verification_artifacts.sh index 1b6ffa2eb..532ab6d27 100755 --- a/scripts/refresh_verification_artifacts.sh +++ b/scripts/refresh_verification_artifacts.sh @@ -11,6 +11,7 @@ python3 scripts/generate_verify_sync_spec.py python3 scripts/generate_evmyullean_capability_report.py python3 scripts/generate_evmyullean_native_lowering_report.py python3 scripts/generate_print_axioms.py +python3 scripts/generate_trust_surface_report.py python3 scripts/sync_verification_status_doc.py echo "[refresh] Validating refreshed artifacts" @@ -20,6 +21,7 @@ python3 scripts/generate_verify_sync_spec.py --check python3 scripts/generate_evmyullean_capability_report.py --check python3 scripts/generate_evmyullean_native_lowering_report.py --check python3 scripts/generate_print_axioms.py --check +python3 scripts/generate_trust_surface_report.py --check python3 scripts/check_verification_status_doc.py python3 scripts/check_layer2_boundary_catalog_sync.py diff --git a/scripts/verify_sync_spec.json b/scripts/verify_sync_spec.json index a7365ce46..bab7906aa 100644 --- a/scripts/verify_sync_spec.json +++ b/scripts/verify_sync_spec.json @@ -7,6 +7,7 @@ "docs-site/**", "Makefile", "AXIOMS.md", + "PrintAxioms.lean", "README.md", "TRUST_ASSUMPTIONS.md" ], @@ -817,6 +818,7 @@ "python3 scripts/generate_evmyullean_capability_report.py --check", "python3 scripts/generate_evmyullean_native_lowering_report.py --check", "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", diff --git a/scripts/verify_sync_spec_source.py b/scripts/verify_sync_spec_source.py index 4baa0d624..fa34e47b3 100644 --- a/scripts/verify_sync_spec_source.py +++ b/scripts/verify_sync_spec_source.py @@ -16,6 +16,7 @@ 'docs-site/**', 'Makefile', 'AXIOMS.md', + 'PrintAxioms.lean', 'README.md', 'TRUST_ASSUMPTIONS.md'], 'build_paths': ['.github/actions/**', @@ -700,8 +701,9 @@ '--check', '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/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 -m unittest discover -s scripts -p 'test_*.py' -v"], From 89048108528328a10caf290a99a8f57b8a2bf17d Mon Sep 17 00:00:00 2001 From: Thomas Marchand Date: Sun, 20 Sep 2026 12:45:04 +0200 Subject: [PATCH 43/50] registry: ABI-encode callback args; live calldata in *_registry Generated *_entrypoint predicates now constrain CallbackContext.calldata with abiEncodeDispatchArgs/ToDispatchVal (compiled-dispatch ABI), not ExternalArg.toWords. Registry executables rewrite calldatasize/calldataload to state-backed live ops so callbacks that branch on calldata are registered. --- AUDIT.md | 16 ++ Contracts/Common.lean | 11 ++ TRUST_ASSUMPTIONS.md | 36 +++- Verity/Core/Model/CallbackBridge.lean | 163 +++++++++++++++++- Verity/Macro/Translate.lean | 40 +++-- .../Model/GeneratedEntrypointRegistry.lean | 75 +++++++- 6 files changed, 316 insertions(+), 25 deletions(-) diff --git a/AUDIT.md b/AUDIT.md index b8a644de9..d8ede44a8 100644 --- a/AUDIT.md +++ b/AUDIT.md @@ -769,6 +769,22 @@ sibling entrypoint. `min_fork` exceeds the contract target fail unless the caller passes `--allow-future-fork-intrinsics`. +## Generated registry ABI calldata (PR #2406) + +- Generated `*_entrypoint` predicates encode Lean arguments with + `abiEncodeDispatchArgs` / `ToDispatchVal` (compiled-dispatch ABI words: + offset + length + packed bytes), not `ExternalArg.toWords` (journal + encoding, one word per byte). +- Generated `*_registry` executables rewrite `calldatasize`/`calldataload` + to `calldatasizeLive`/`calldataloadLive`, which read + `ContractState.calldata` installed by `withCallbackContext`. Public + stubs remain `0` / the offset. +- Evidence: `Verity/Proofs/Model/GeneratedEntrypointRegistry.lean` + (`RegistryDispatchCalldata`, `RegistryLiveCalldata`). +- Trust docs: `TRUST_ASSUMPTIONS.md` (executable-plane stubs are closed + definitions; registry completeness remains an author obligation). +- Axiom-free; no `sorry`/`admit`/`native_decide`. + ## Update Checklist 1. Update `TRUST_ASSUMPTIONS.md` for the human-readable trust boundary. diff --git a/Contracts/Common.lean b/Contracts/Common.lean index d1c36268e..c765db384 100644 --- a/Contracts/Common.lean +++ b/Contracts/Common.lean @@ -393,6 +393,17 @@ def tryCatchWord (attempt : Uint256) (handler : String → Contract Unit) : Cont def calldatasize : Uint256 := 0 def returndataSize : Uint256 := 0 def calldataload (offset : Uint256) : Uint256 := offset +/-- Registry-mode executable calldata. Public `calldatasize`/`calldataload` +remain deterministic stubs; generated `*_registry` bodies use these so a +callback that branches on live calldata is registered. Byte offset 0 of the +word-list data region is `calldataload 4`, matching compiled dispatch. -/ +def calldatasizeLive : Contract Uint256 := fun state => + ContractResult.success state.calldataSize state +def calldataloadLive (offset : Uint256) : Contract Uint256 := fun state => + ContractResult.success + (Verity.Core.Uint256.ofNat + (Compiler.CompilationModel.Denote.calldataloadWord 0 state.calldata offset.val)) + state def mload (offset : Uint256) : Uint256 := offset def tload (offset : Uint256) : Contract Uint256 := fun state => ContractResult.success (state.transientStorage (offset : Nat)) state diff --git a/TRUST_ASSUMPTIONS.md b/TRUST_ASSUMPTIONS.md index 4119beae1..343c8e1a9 100644 --- a/TRUST_ASSUMPTIONS.md +++ b/TRUST_ASSUMPTIONS.md @@ -483,10 +483,12 @@ of byte-for-byte EVM ABI layout. Trust boundaries of that plane: - **Return values are deterministic stubs, not adversary models.** In-band words come from `externalCallStubWord`; the success bit is `externalCallStubSuccess` (`false` only for the reserved callee name - `"fail"`). Supported single-word results decode that same word; aggregate - and no-result stubs use their inhabited default. Executable-plane theorems - about call *outcomes* are therefore claims about the stub, not about a real - callee; adversarial reasoning lives in the model plane (`DenoteExternalCalls`). + `"fail"`). These are closed Lean definitions, not feature-flag, env-var, or + backend overrides. Supported single-word results decode that same word; + aggregate and no-result stubs use their inhabited default. Executable-plane + theorems about call *outcomes* are therefore claims about the stub, not about + a real callee; adversarial reasoning lives in the model plane + (`DenoteExternalCalls`). - **`ExecutableCallContext.ofAdversary` pins `target = 0` and `value = 0`.** It is a convenience context, not a general linker: a nonzero target or value requires `ofCallEnv` or a custom `resolve`. The generated registry predicate @@ -495,6 +497,15 @@ of byte-for-byte EVM ABI layout. Trust boundaries of that plane: carrying the registry adversary (including `ofCallEnv`) is a registered transition. `CallbackBounded` therefore covers executable linked calls that resolve a nonzero target/value, not only the zero boundary. +- **Registry callback calldata is compiled-dispatch ABI, not the journal + encoder.** Generated `*_entrypoint` predicates constrain + `CallbackContext.calldata` with `abiEncodeDispatchArgs` / + `ToDispatchVal` (offset/length/packed-bytes layout matching + `genParamLoads`). `ExternalArg.toWords` remains the executable journal + encoding and is not an ABI decoder. Public `calldatasize`/`calldataload` + stay deterministic stubs (`0` and the offset); generated `*_registry` + bodies rewrite those intrinsics to `calldatasizeLive`/`calldataloadLive`, + which read `ContractState.calldata` installed by `withCallbackContext`. - **`externalCallWords` (pure expression form) does not journal.** It is not monadic, so `externalCall name [args]` used as a pure expression remains observationally silent; only the monadic forms journal. Specs that need @@ -643,10 +654,19 @@ global invariant `I : ContractState → Prop`. definition takes the context, including view/static callees whose public bodies do not open a reentrancy window. Public hopCall bodies keep the ctx-free public definition. Generated `*_entrypoint` predicates require - Lean arguments to ABI-decode from the same `CallbackContext.calldata` - (`dispatchCalldataMatches`); `receive` is registered only for empty - calldata. Same-contract `selfCall` hops keep the guarded public / - `*_registry` path so they still observe the nonReentrant tload prologue. + Lean arguments to match compiled-dispatch ABI words of + `CallbackContext.calldata` (`abiEncodeDispatchArgs` into + `dispatchCalldataMatches`); `receive` is registered only for empty + calldata. Registry-mode executables observe that same calldata through + `calldatasizeLive`/`calldataloadLive`. Completeness of the generated + list is still an author obligation: the kernel checks consumers of + `entrypointRegistry`, it does not independently re-enumerate compiled + dispatcher cases. Same-contract `selfCall` hops keep the guarded public + / `*_registry` path so they still observe the nonReentrant tload + prologue. The Lean toolchain is pinned by `lean-toolchain`; + `threadAdversaryThroughExecutableSyntax` consults no IO or env vars. + Existing `unsafe qualifiedTupleBindTypedLocals` is elaborator-only + typed-local inference, not a kernel skip. Author-supplied lists remain only for hand-written `ReentrancySpec` consumers. 2. **Adversary-model fidelity** — reentry is modeled as an arbitrary diff --git a/Verity/Core/Model/CallbackBridge.lean b/Verity/Core/Model/CallbackBridge.lean index df2dff56f..4a5aa1c2e 100644 --- a/Verity/Core/Model/CallbackBridge.lean +++ b/Verity/Core/Model/CallbackBridge.lean @@ -52,10 +52,171 @@ structure CallbackContext where calldataSize : Verity.Uint256 calldata : List Nat +/-- Packed ABI bytes/string data: 32-byte big-endian words, right-zero-padded. +Not `ExternalArg.toWords`, which journals one word per byte. Fuel is +`bytes.length`, so this is structurally recursive on `Nat` and reduces +under `decide`. -/ +def packAbiBytes (bytes : List Nat) : List Nat := + packAbiBytesFuel bytes.length bytes +where + packAbiBytesFuel : Nat → List Nat → List Nat + | 0, _ => [] + | _n+1, [] => [] + | n+1, x :: xs => + let rest := x :: xs + let chunk := rest.take 32 + let padded := chunk ++ List.replicate (32 - chunk.length) 0 + let word := padded.foldl (fun acc b => acc * 256 + b % 256) 0 + word :: packAbiBytesFuel n (rest.drop 32) + +/-- One ABI argument as consumed by `genParamLoads` / compiled dispatch. -/ +inductive DispatchVal where + | word : Nat → DispatchVal + | bytes : List Nat → DispatchVal + | array : List DispatchVal → DispatchVal + | tuple : List DispatchVal → DispatchVal + +mutual + def DispatchVal.isDynamic : DispatchVal → Bool + | .word _ => false + | .bytes _ => true + | .array _ => true + | .tuple vs => dispatchValAnyDynamic vs + + def dispatchValAnyDynamic : List DispatchVal → Bool + | [] => false + | v :: vs => v.isDynamic || dispatchValAnyDynamic vs + + def DispatchVal.headBytes : DispatchVal → Nat + | .word _ => 32 + | .bytes _ => 32 + | .array _ => 32 + | .tuple vs => + if dispatchValAnyDynamic vs then 32 + else dispatchValHeadBytesList vs + + def dispatchValHeadBytesList : List DispatchVal → Nat + | [] => 0 + | v :: vs => v.headBytes + dispatchValHeadBytesList vs + + /-- Payload words of a value (no parent offset). Dynamic arrays of dynamic + elements use offsets relative to the start of the post-length head. -/ + def DispatchVal.payloadWords : DispatchVal → List Nat + | .word w => [w] + | .bytes bs => bs.length :: packAbiBytes bs + | .array vs => + if dispatchValAnyDynamic vs then + let (offs, tails) := encodeDynamicList vs (32 * vs.length) + vs.length :: offs ++ tails + else + vs.length :: dispatchValFlatPayload vs + | .tuple vs => + if dispatchValAnyDynamic vs then + let (heads, tails) := encodeArgBlock vs (dispatchValHeadBytesList vs) + heads ++ tails + else + dispatchValFlatPayload vs + + def dispatchValFlatPayload : List DispatchVal → List Nat + | [] => [] + | v :: vs => v.payloadWords ++ dispatchValFlatPayload vs + + def encodeDynamicList : List DispatchVal → Nat → List Nat × List Nat + | [], _ => ([], []) + | v :: vs, tailOff => + let pay := v.payloadWords + let (offs, tails) := encodeDynamicList vs (tailOff + pay.length * 32) + (tailOff :: offs, pay ++ tails) + + def encodeArgBlock : List DispatchVal → Nat → List Nat × List Nat + | [], _ => ([], []) + | v :: vs, tailOff => + if v.isDynamic then + let pay := v.payloadWords + let (heads, tails) := encodeArgBlock vs (tailOff + pay.length * 32) + (tailOff :: heads, pay ++ tails) + else + let (heads, tails) := encodeArgBlock vs tailOff + (v.payloadWords ++ heads, tails) +end + +/-- True when every argument is a static ABI word. Kept outside the mutual +block so `decide` unfolds it. -/ +def dispatchArgsAllWords : List DispatchVal → Bool + | [] => true + | .word _ :: rest => dispatchArgsAllWords rest + | _ => false + +def dispatchArgWordVals : List DispatchVal → List Nat + | [] => [] + | .word w :: rest => w :: dispatchArgWordVals rest + | _ :: rest => 0 :: dispatchArgWordVals rest + +/-- ABI argument-block encoding matching `genParamLoads`: static values occupy +head words; dynamic values contribute a head offset then a tail of +`[length, packed data…]` (bytes/string) or `[length, elements…]` (arrays). +All-static-word argument lists skip the mutual encoder so kernel `decide` +reduces generated scalar `*_entrypoint` tests. -/ +def abiEncodeDispatchArgs (args : List DispatchVal) : List Nat := + if dispatchArgsAllWords args then + dispatchArgWordVals args + else + let (heads, tails) := encodeArgBlock args (dispatchValHeadBytesList args) + heads ++ tails + +@[simp] theorem abiEncodeDispatchArgs_nil : + abiEncodeDispatchArgs [] = [] := rfl + +@[simp] theorem abiEncodeDispatchArgs_singleton_word (w : Nat) : + abiEncodeDispatchArgs [.word w] = [w] := rfl + +class ToDispatchVal (α : Type) where + toDispatchVal : α → DispatchVal + +instance : ToDispatchVal Verity.Uint256 where + toDispatchVal v := .word v.val + +instance : ToDispatchVal Verity.Uint16 where + toDispatchVal v := .word v.toUint256.val + +instance : ToDispatchVal (Verity.UIntN bits) where + toDispatchVal v := .word v.toUint256.val + +instance : ToDispatchVal (Verity.IntN bits) where + toDispatchVal v := .word v.toUint256.val + +instance : ToDispatchVal (Verity.BytesN bytes) where + toDispatchVal v := .word v.toUint256.val + +instance : ToDispatchVal Verity.Int256 where + toDispatchVal v := .word v.word.val + +instance : ToDispatchVal Verity.Address where + toDispatchVal v := .word v.val + +instance : ToDispatchVal Bool where + toDispatchVal v := .word (if v then 1 else 0) + +instance : ToDispatchVal Nat where + toDispatchVal v := .word v + +instance : ToDispatchVal ByteArray where + toDispatchVal b := .bytes (b.data.toList.map (fun x => x.toNat)) + +instance : ToDispatchVal String where + toDispatchVal s := ToDispatchVal.toDispatchVal s.toUTF8 + +instance [ToDispatchVal α] : ToDispatchVal (Array α) where + toDispatchVal vs := .array (vs.toList.map ToDispatchVal.toDispatchVal) + +instance [ToDispatchVal α] [ToDispatchVal β] : ToDispatchVal (α × β) where + toDispatchVal p := .tuple [ToDispatchVal.toDispatchVal p.1, ToDispatchVal.toDispatchVal p.2] + /-- Compiled dispatch ABI-decodes arguments from the same calldata that selected the function (`calldataload` at 4 + 32*i, `calldatasize` at least 4 + 32 * n). Extra trailing words are allowed, matching Yul -`calldatasizeGuard`. -/ +`calldatasizeGuard`. `argWords` is the ABI data region (no 4-byte selector), +from `abiEncodeDispatchArgs`, not `ExternalArg.toWords`. -/ def dispatchCalldataMatches (ctx : CallbackContext) (argWords : List Nat) : Prop := ctx.calldata.take argWords.length = argWords ∧ Verity.Core.Uint256.ofNat (4 + 32 * argWords.length) ≤ ctx.calldataSize diff --git a/Verity/Macro/Translate.lean b/Verity/Macro/Translate.lean index 6fe940687..e3abb1b07 100644 --- a/Verity/Macro/Translate.lean +++ b/Verity/Macro/Translate.lean @@ -3072,6 +3072,16 @@ private def rewriteLinkedCallTerm `(term| evmDelegateCallWords $adv $gas $target $inOffset $inSize $outOffset $outSize) | `(term| returnDataSize()) | `(term| returndataSize) => `(term| returndataSizeLive) + | `(term| calldatasize) => + if registryMode then + `(term| calldatasizeLive) + else + pure stx + | `(term| calldataload $offset:term) => + if registryMode then + `(term| calldataloadLive $offset) + else + pure stx | `(term| returnDataCopy($destOffset, $sourceOffset, $size)) | `(term| returndataCopy $destOffset $sourceOffset $size) => `(term| returndataCopyLive $destOffset $sourceOffset $size) @@ -3138,7 +3148,7 @@ private def rewriteLinkedCallTerm | some rewritten => pure rewritten | none => pure other -private def isLiveStateExternalCall (stx : Term) : Bool := +private def isLiveStateExternalCall (stx : Term) (registryMode : Bool := false) : Bool := match stx with | `(term| __verityTypedCall $_ $_ [ $[$_],* ]) | `(term| __verityTypedEffect $_ $_ [ $[$_],* ]) @@ -3168,6 +3178,7 @@ private def isLiveStateExternalCall (stx : Term) : Bool := | `(term| safeApprove $_ $_ $_) | `(term| legacyStringSafeTransfer $_ $_ $_) | `(term| legacyStringSafeTransferFrom $_ $_ $_ $_) => true + | `(term| calldatasize) | `(term| calldataload $_) => registryMode | _ => false /-- Restore the source language's word-like coercions after a live external @@ -3379,7 +3390,7 @@ private partial def threadAdversaryThroughExecutableSyntax binds := binds ++ inner newArgs := newArgs.set! i nt.raw let rebuilt ← adaptHoistedWordContext ⟨Syntax.node info kind newArgs⟩ - if isLiveStateExternalCall rebuilt then + if isLiveStateExternalCall rebuilt registryMode then bindCall binds rebuilt else match ← rewriteTypedInterfaceCall? externalDecls params adv @@ -3392,7 +3403,7 @@ private partial def threadAdversaryThroughExecutableSyntax pure (binds, rewritten) | none => pure (binds, rebuilt) | _ => - if isLiveStateExternalCall t then + if isLiveStateExternalCall t registryMode then bindCall #[] t else match ← rewriteTypedInterfaceCall? externalDecls params adv @@ -3714,13 +3725,13 @@ private partial def threadAdversaryThroughExecutableSyntax helpers adversarialHelpers registryOnlyHelpers params locals name original adv with | some app => pure app.raw | none => - if isLiveStateExternalCall ⟨stx⟩ then + if isLiveStateExternalCall ⟨stx⟩ registryMode then (·.raw) <$> rewriteLinkedCallTerm externalDecls params adv (linkedContracts := linkedContracts) (registryMode := registryMode) ⟨stx⟩ else recurseChildren | _ => let asTerm : Term := ⟨stx⟩ - if isLiveStateExternalCall asTerm then + if isLiveStateExternalCall asTerm registryMode then (·.raw) <$> rewriteLinkedCallTerm externalDecls params adv (linkedContracts := linkedContracts) (registryMode := registryMode) asTerm else recurseChildren @@ -6760,23 +6771,26 @@ def mkFunctionCommandsPublic $context:ident $applied) if !fn.isPayable then registryBody ← `(($context:ident).msgValue = 0 ∧ $registryBody) - -- Tie Lean arguments to the same calldata the compiled dispatcher + -- Tie Lean arguments to the same ABI calldata the compiled dispatcher -- ABI-decodes (`calldatasizeGuard` + `calldataload`). `receive` is - -- compiled only when `calldatasize == 0`. + -- compiled only when `calldatasize == 0`. Dynamic types use + -- `abiEncodeDispatchArgs`, not `ExternalArg.toWords`. if fn.name == "receive" then registryBody ← `(Compiler.CompilationModel.DenoteExternalCalls.receiveCalldataMatches $context:ident ∧ $registryBody) else if fn.name != "fallback" then - let mut argWordTerms : Array Term := #[] + let mut dispatchValTerms : Array Term := #[] for (paramIdent, _) in registryParams do - let words ← `(List.map (fun w => (w : Nat)) (Contracts.ExternalArg.toWords $paramIdent:ident)) - argWordTerms := argWordTerms.push words + dispatchValTerms := dispatchValTerms.push + (← `(Compiler.CompilationModel.DenoteExternalCalls.ToDispatchVal.toDispatchVal + $paramIdent:ident)) let argWordsTerm ← - if argWordTerms.isEmpty then + if dispatchValTerms.isEmpty then `( ([] : List Nat) ) else - `(List.flatten ([ $[$argWordTerms],* ] : List (List Nat))) + `(Compiler.CompilationModel.DenoteExternalCalls.abiEncodeDispatchArgs + ([ $[$dispatchValTerms],* ] : List Compiler.CompilationModel.DenoteExternalCalls.DispatchVal)) registryBody ← `(Compiler.CompilationModel.DenoteExternalCalls.dispatchCalldataMatches $context:ident $argWordsTerm ∧ $registryBody) @@ -6826,7 +6840,7 @@ def mkFunctionCommandsPublic /-- Emit the contract-wide union of all externally callable entrypoint predicates. Each per-function predicate keeps arguments existential, ties -them to the same calldata compiled dispatch ABI-decodes, and uses the +them to compiled-dispatch ABI calldata (`abiEncodeDispatchArgs`), and uses the registry's explicit adversary when the function opens a reentrancy window. -/ def mkEntrypointRegistryCommandPublic (functions : Array FunctionDecl) : CommandElabM Cmd := do let advIdent ← Lean.Elab.Term.mkFreshIdent (mkIdent `_registryAdv).raw diff --git a/Verity/Proofs/Model/GeneratedEntrypointRegistry.lean b/Verity/Proofs/Model/GeneratedEntrypointRegistry.lean index b73ae7e2c..d7e79ff99 100644 --- a/Verity/Proofs/Model/GeneratedEntrypointRegistry.lean +++ b/Verity/Proofs/Model/GeneratedEntrypointRegistry.lean @@ -36,7 +36,7 @@ The registry quantifies over the executable resolver, so any theorem guardedPing_registered (ectx : Contracts.ExecutableCallContext) (ctx : CallbackContext) (value : Uint256) (hvalue : ctx.msgValue = 0) (hcalldata : dispatchCalldataMatches ctx - (List.map (fun w => (w : Nat)) (Contracts.ExternalArg.toWords value))) : + (abiEncodeDispatchArgs [ToDispatchVal.toDispatchVal value])) : entrypointRegistry ectx.adversary (callbackContractTransition ctx (guardedPing_registry ectx value)) := by left @@ -46,7 +46,7 @@ theorem guardedPing_registered (ectx : Contracts.ExecutableCallContext) (ctx : C theorem guardedPing_registered_ofAdversary (adv : AdversaryModel) (ctx : CallbackContext) (value : Uint256) (hvalue : ctx.msgValue = 0) (hcalldata : dispatchCalldataMatches ctx - (List.map (fun w => (w : Nat)) (Contracts.ExternalArg.toWords value))) : + (abiEncodeDispatchArgs [ToDispatchVal.toDispatchVal value])) : entrypointRegistry adv (callbackContractTransition ctx (guardedPing_registry (Contracts.ExecutableCallContext.ofAdversary adv) value)) := @@ -284,7 +284,7 @@ def nonemptyReceiveCtx : CallbackContext where calldata := [1] def argWords (value : Uint256) : List Nat := - List.map (fun w => (w : Nat)) (Contracts.ExternalArg.toWords value) + abiEncodeDispatchArgs [ToDispatchVal.toDispatchVal value] example : dispatchCalldataMatches (matchingCtx 7) (argWords 7) := by decide @@ -331,8 +331,77 @@ theorem receive_entrypoint_requires_empty (__verity_receive_registry { adversary := adv, resolve := resolve }) := h +/-- Journal encoding of `Bytes` is one word per byte (`[len, b0, b1, …]`). +Compiled dispatch marks bytes as dynamic (`DispatchVal.bytes`) and packs +them as `[length, packed data…]` via `abiEncodeDispatchArgs`. -/ +example : dispatchArgsAllWords + [ToDispatchVal.toDispatchVal (ByteArray.mk #[0x61, 0x62])] = false := + rfl + +example : + List.map (fun w => (w : Nat)) + (Contracts.ExternalArg.toWords (ByteArray.mk #[0x61, 0x62])) = + [2, 0x61, 0x62] := by + decide + end RegistryDispatchCalldata +/-! Regression: registry executables observe live callback calldata, not the +public `calldatasize = 0` / `calldataload offset = offset` stubs. -/ +verity_contract RegistryLiveCalldata where + storage + last : Uint256 := slot 0 + + function setFromCalldata (_value : Uint256) + local_obligations [manual_low_level_refinement := assumed + "Fixture reads compiled-dispatch calldata via calldataload 4."] + : Unit := do + let cds := calldatasize + let loaded := calldataload 4 + if cds == 36 then + setStorage last loaded + else + setStorage last 0 + return () + +namespace RegistryLiveCalldata + +def liveCtx (value : Uint256) : CallbackContext where + sender := 0 + msgValue := 0 + calldataSize := Verity.Core.Uint256.ofNat 36 + calldata := [value.val] + +def publicWritesStub : Bool := + match (setFromCalldata (7 : Uint256)).run Verity.defaultState with + | .success _ s => s.storage 0 == 0 + | _ => false + +example : publicWritesStub = true := by decide + +def liveWritesLoaded : Bool := + let s := callbackContractTransition (liveCtx 7) + (setFromCalldata_registry + (Contracts.ExecutableCallContext.ofAdversary AdversaryModel.stub) 7) + Verity.defaultState + s.storage 0 == 7 + +example : liveWritesLoaded = true := by decide + +def liveArgWords (value : Uint256) : List Nat := + abiEncodeDispatchArgs [ToDispatchVal.toDispatchVal value] + +theorem setFromCalldata_registered_live + (h : dispatchCalldataMatches (liveCtx 7) (liveArgWords 7)) : + setFromCalldata_entrypoint AdversaryModel.stub + (callbackContractTransition (liveCtx 7) + (setFromCalldata_registry + (Contracts.ExecutableCallContext.ofAdversary AdversaryModel.stub) 7)) := + ⟨liveCtx 7, (Contracts.ExecutableCallContext.ofAdversary AdversaryModel.stub).resolve, + 7, h, rfl, rfl⟩ + +end RegistryLiveCalldata + /-! Regression: public Solidity self-calls still hit the nonReentrant tload prologue. Bound hops must keep the guarded registry path, not `*_unguarded`. -/ verity_contract RegistrySelfCallGuard where From e3b5e659b5550a114e132aac81971287fba01d83 Mon Sep 17 00:00:00 2001 From: "github-actions[bot]" Date: Sun, 20 Sep 2026 12:46:16 +0200 Subject: [PATCH 44/50] chore: auto-refresh derived artifacts --- PrintAxioms.lean | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/PrintAxioms.lean b/PrintAxioms.lean index ea897d0bb..85e25df09 100644 --- a/PrintAxioms.lean +++ b/PrintAxioms.lean @@ -753,6 +753,7 @@ end Verity.AxiomAudit Contracts.ReentrancyRelyGuarantee.RegistryDispatchCalldata.setLast_entrypoint_requires_dispatch Contracts.ReentrancyRelyGuarantee.RegistryDispatchCalldata.receive_registered_empty Contracts.ReentrancyRelyGuarantee.RegistryDispatchCalldata.receive_entrypoint_requires_empty + Contracts.ReentrancyRelyGuarantee.RegistryLiveCalldata.setFromCalldata_registered_live Contracts.ReentrancyRelyGuarantee.generated_registry_callback_preserves -- Verity/Proofs/Stdlib/Automation.lean @@ -7620,4 +7621,4 @@ end Verity.AxiomAudit Compiler.Proofs.YulGeneration.YulTransaction.ofIR_args ] --- Total: 7037 theorems/lemmas (5024 public, 2013 private, 0 sorry'd) +-- Total: 7038 theorems/lemmas (5025 public, 2013 private, 0 sorry'd) From 1016f43d599452c6ea2b24b66470d814a8e453e5 Mon Sep 17 00:00:00 2001 From: Thomas Marchand Date: Sun, 20 Sep 2026 13:37:20 +0200 Subject: [PATCH 45/50] macro: emit ToDispatchVal for named structs Generated *_entrypoint predicates encode args with ToDispatchVal. Named/nested struct parameters (FeeConfig, Outer, Array Transaction) had no instance, so lake build Contracts.Smoke.StructsAndArrays failed in Verify proofs. Emit a tuple-of-fields instance next to ExternalArg. --- Verity/Macro/Elaborate.lean | 1 + Verity/Macro/Translate.lean | 15 +++++++++++++++ 2 files changed, 16 insertions(+) diff --git a/Verity/Macro/Elaborate.lean b/Verity/Macro/Elaborate.lean index 917a6ff95..62c913b12 100644 --- a/Verity/Macro/Elaborate.lean +++ b/Verity/Macro/Elaborate.lean @@ -136,6 +136,7 @@ private def elabVerityContractOrMixin (stx : Syntax) : CommandElabM Unit := do elabCommand (← mkStructDefCommandPublic structDecl) elabCommand (← mkStructEventArgInstanceCommandPublic structDecl) elabCommand (← mkStructExternalArgInstanceCommandPublic structDecl) + elabCommand (← mkStructToDispatchValInstanceCommandPublic structDecl) elabCommand (← mkStructExternalResultInstanceCommandPublic structDecl) let aliasCmds ← mkIncludeAliasCommandsPublic resolvedIncludes diff --git a/Verity/Macro/Translate.lean b/Verity/Macro/Translate.lean index e3abb1b07..ad3c3622b 100644 --- a/Verity/Macro/Translate.lean +++ b/Verity/Macro/Translate.lean @@ -5986,6 +5986,21 @@ def mkStructExternalArgInstanceCommandPublic (decl : StructDecl) : CommandElabM ([ $[$encodedFields],* ] : List (List _root_.Verity.Uint256))) +/-- Compiled-dispatch ABI encoding of a named struct: the tuple of its +fields, matching `genParamLoads` / `abiEncodeDispatchArgs`. -/ +def mkStructToDispatchValInstanceCommandPublic (decl : StructDecl) : CommandElabM Cmd := do + let structId := decl.ident + let valueId := mkIdent (Name.mkSimple "value") + let fieldIds := decl.fields.map (·.ident) + let encodedFields ← fieldIds.mapM fun fieldId => + `(term| Compiler.CompilationModel.DenoteExternalCalls.ToDispatchVal.toDispatchVal + $valueId.$fieldId) + `(command| instance : Compiler.CompilationModel.DenoteExternalCalls.ToDispatchVal $structId where + toDispatchVal := fun $valueId => + Compiler.CompilationModel.DenoteExternalCalls.DispatchVal.tuple + ([ $[$encodedFields],* ] : + List Compiler.CompilationModel.DenoteExternalCalls.DispatchVal)) + def mkStructExternalResultInstanceCommandPublic (decl : StructDecl) : CommandElabM Cmd := do let structId := decl.ident let fieldIds := decl.fields.map (·.ident) From 1372b573c9c669433348b7d6919ad7ae9b44b254 Mon Sep 17 00:00:00 2001 From: Thomas Marchand Date: Mon, 21 Sep 2026 03:20:52 +0000 Subject: [PATCH 46/50] registry: preserve selector, flatten ABI, normalize scalars, route calldata helpers Fix four Codex P1s on #2406: live calldataload observes CallbackContext.selector, FixedArray/Tuple DispatchVal matches genParamLoads, dispatchCalldataMatchesKinds accepts genScalarLoad-normalized words, and calldata-reading helpers go through *_registry. Tests in GeneratedEntrypointRegistry. Completeness remains an author obligation. --- AUDIT.md | 7 +- Contracts/Common.lean | 7 +- TRUST_ASSUMPTIONS.md | 10 +- Verity/Core.lean | 3 + Verity/Core/Model/CallbackBridge.lean | 90 ++++++++++- Verity/Macro/Translate.lean | 149 ++++++++++++++++-- .../Model/GeneratedEntrypointRegistry.lean | 144 +++++++++++++++++ 7 files changed, 391 insertions(+), 19 deletions(-) diff --git a/AUDIT.md b/AUDIT.md index d8ede44a8..d2da61776 100644 --- a/AUDIT.md +++ b/AUDIT.md @@ -777,8 +777,11 @@ sibling entrypoint. encoding, one word per byte). - Generated `*_registry` executables rewrite `calldatasize`/`calldataload` to `calldatasizeLive`/`calldataloadLive`, which read - `ContractState.calldata` installed by `withCallbackContext`. Public - stubs remain `0` / the offset. + `ContractState.calldata` and `ContractState.selector` installed by + `withCallbackContext`. Public stubs remain `0` / the offset. + `dispatchCalldataMatches` normalizes scalar words like `genScalarLoad`. + `FixedArray` / flat `Tuple` encodings match `genParamLoads`. Calldata-reading + helpers are routed through `*_registry`. - Evidence: `Verity/Proofs/Model/GeneratedEntrypointRegistry.lean` (`RegistryDispatchCalldata`, `RegistryLiveCalldata`). - Trust docs: `TRUST_ASSUMPTIONS.md` (executable-plane stubs are closed diff --git a/Contracts/Common.lean b/Contracts/Common.lean index c765db384..547af8366 100644 --- a/Contracts/Common.lean +++ b/Contracts/Common.lean @@ -396,13 +396,16 @@ def calldataload (offset : Uint256) : Uint256 := offset /-- Registry-mode executable calldata. Public `calldatasize`/`calldataload` remain deterministic stubs; generated `*_registry` bodies use these so a callback that branches on live calldata is registered. Byte offset 0 of the -word-list data region is `calldataload 4`, matching compiled dispatch. -/ +word-list data region is `calldataload 4`, matching compiled dispatch. +`calldataload 0` (and unaligned loads overlapping the first four bytes) +observe `state.selector`, not a hard-coded 0. -/ def calldatasizeLive : Contract Uint256 := fun state => ContractResult.success state.calldataSize state def calldataloadLive (offset : Uint256) : Contract Uint256 := fun state => ContractResult.success (Verity.Core.Uint256.ofNat - (Compiler.CompilationModel.Denote.calldataloadWord 0 state.calldata offset.val)) + (Compiler.CompilationModel.Denote.calldataloadWord + state.selector state.calldata offset.val)) state def mload (offset : Uint256) : Uint256 := offset def tload (offset : Uint256) : Contract Uint256 := fun state => diff --git a/TRUST_ASSUMPTIONS.md b/TRUST_ASSUMPTIONS.md index 343c8e1a9..144cb3fdd 100644 --- a/TRUST_ASSUMPTIONS.md +++ b/TRUST_ASSUMPTIONS.md @@ -658,7 +658,15 @@ global invariant `I : ContractState → Prop`. `CallbackContext.calldata` (`abiEncodeDispatchArgs` into `dispatchCalldataMatches`); `receive` is registered only for empty calldata. Registry-mode executables observe that same calldata through - `calldatasizeLive`/`calldataloadLive`. Completeness of the generated + `calldatasizeLive`/`calldataloadLive`, including the selected + entrypoint selector at `calldataload 0` (`CallbackContext.selector` / + `ContractState.selector`). `dispatchCalldataMatches` accepts + `genScalarLoad`-normalized noncanonical Bool / uintN / address words. + `FixedArray` encodes as a length-free composite (tuple of members); + source `Tuple` encodings are flattened before `abiEncodeDispatchArgs`. + Helpers whose bodies (transitively) read `calldatasize`/`calldataload` + are routed through `*_registry` so they observe live calldata. + Completeness of the generated list is still an author obligation: the kernel checks consumers of `entrypointRegistry`, it does not independently re-enumerate compiled dispatcher cases. Same-contract `selfCall` hops keep the guarded public diff --git a/Verity/Core.lean b/Verity/Core.lean index e0208f0f3..92314cfc4 100644 --- a/Verity/Core.lean +++ b/Verity/Core.lean @@ -337,6 +337,9 @@ structure ContractState where blobBaseFee : Uint256 := 0 calldataSize : Uint256 := 0 calldata : List Nat := [] -- Immutable calldata words used by ABI expression semantics + /-- 4-byte function selector for live `calldataload 0` in registry mode. + Compiled dispatch packs it in the high 4 bytes of the first word. -/ + selector : Nat := 0 memory : Nat → Uint256 := fun _ => 0 -- EVM memory (word-addressed, zero-initialized) knownAddresses : Nat → FiniteAddressSet -- Tracked addresses per storage slot (for sum properties) events : List Event := [] -- Emitted events, append-only log (#153) diff --git a/Verity/Core/Model/CallbackBridge.lean b/Verity/Core/Model/CallbackBridge.lean index 4a5aa1c2e..a07565794 100644 --- a/Verity/Core/Model/CallbackBridge.lean +++ b/Verity/Core/Model/CallbackBridge.lean @@ -51,6 +51,10 @@ structure CallbackContext where msgValue : Verity.Uint256 calldataSize : Verity.Uint256 calldata : List Nat + /-- 4-byte selector of the selected entrypoint. Live `calldataload 0` + observes this via `ContractState.selector`. Defaults to 0 so existing + fixtures that omit it stay well-typed. -/ + selector : Nat := 0 /-- Packed ABI bytes/string data: 32-byte big-endian words, right-zero-padded. Not `ExternalArg.toWords`, which journals one word per byte. Fuel is @@ -141,7 +145,8 @@ mutual end /-- True when every argument is a static ABI word. Kept outside the mutual -block so `decide` unfolds it. -/ +block so `decide` unfolds it. Static tuples/fixed arrays use `payloadWords` +via the mutual encoder (not this fast path). -/ def dispatchArgsAllWords : List DispatchVal → Bool | [] => true | .word _ :: rest => dispatchArgsAllWords rest @@ -209,18 +214,84 @@ instance : ToDispatchVal String where instance [ToDispatchVal α] : ToDispatchVal (Array α) where toDispatchVal vs := .array (vs.toList.map ToDispatchVal.toDispatchVal) +/-- Right-nested Lean products (`α × (β × γ)`) encode as a nested ABI tuple +unless flattened. Compiled `Tuple [α, β, γ]` is a single flat tuple. -/ +def flattenDispatchTuple : DispatchVal → List DispatchVal + | .tuple vs => vs.flatMap flattenDispatchTuple + | v => [v] + +/-- Compiled `T[n]` is a static/dynamic composite with no `T[]` length word. +Encode as a tuple of n members (inlined if static; offset-only if dynamic). -/ +def dispatchFixedArray (elems : List DispatchVal) : DispatchVal := + .tuple elems + +/-- Flatten a right-nested product encoding into one ABI tuple. -/ +def dispatchFlatTuple (v : DispatchVal) : DispatchVal := + .tuple (flattenDispatchTuple v) + instance [ToDispatchVal α] [ToDispatchVal β] : ToDispatchVal (α × β) where - toDispatchVal p := .tuple [ToDispatchVal.toDispatchVal p.1, ToDispatchVal.toDispatchVal p.2] + toDispatchVal p := + .tuple (flattenDispatchTuple (ToDispatchVal.toDispatchVal p.1) ++ + flattenDispatchTuple (ToDispatchVal.toDispatchVal p.2)) + +/-- How `genScalarLoad` normalizes a loaded word before the body sees it. -/ +inductive ScalarLoadKind where + | identity + | bool + | uint8 + | uint16 + | uintN (bits : Nat) + | address + | bytesN (bytes : Nat) + deriving Repr, DecidableEq + +def normalizeLoadedWord (kind : ScalarLoadKind) (loaded : Nat) : Nat := + let w := loaded % Compiler.Constants.evmModulus + match kind with + | .identity => w + | .bool => if w == 0 then 0 else 1 + | .uint8 => w % 256 + | .uint16 => w % 65536 + | .uintN bits => w % (2 ^ bits) + | .address => w &&& Compiler.Constants.addressMask + | .bytesN bytes => + w &&& ((2 ^ (8 * bytes) - 1) * 2 ^ (8 * (32 - bytes))) + +def scalarWordMatches (kind : ScalarLoadKind) (canonical loaded : Nat) : Bool := + normalizeLoadedWord kind loaded == canonical % Compiler.Constants.evmModulus + +def dispatchWordsMatch : List ScalarLoadKind → List Nat → List Nat → Bool + | [], _, _ => true + | _ :: _, [], _ => false + | _ :: _, _ :: _, [] => false + | k :: ks, c :: cs, l :: ls => + scalarWordMatches k c l && dispatchWordsMatch ks cs ls /-- Compiled dispatch ABI-decodes arguments from the same calldata that selected the function (`calldataload` at 4 + 32*i, `calldatasize` at least 4 + 32 * n). Extra trailing words are allowed, matching Yul `calldatasizeGuard`. `argWords` is the ABI data region (no 4-byte selector), -from `abiEncodeDispatchArgs`, not `ExternalArg.toWords`. -/ +from `abiEncodeDispatchArgs`, not `ExternalArg.toWords`. Scalar prefixes +compare after `genScalarLoad` normalization so noncanonical Bool/uintN/ +address words still register. -/ def dispatchCalldataMatches (ctx : CallbackContext) (argWords : List Nat) : Prop := - ctx.calldata.take argWords.length = argWords ∧ + dispatchWordsMatch + (List.replicate argWords.length ScalarLoadKind.identity) + argWords (ctx.calldata.take argWords.length) = true ∧ Verity.Core.Uint256.ofNat (4 + 32 * argWords.length) ≤ ctx.calldataSize +/-- Like `dispatchCalldataMatches`, but scalar words compare after the compiled +`genScalarLoad` normalization for each argument. -/ +def dispatchCalldataMatchesKinds (ctx : CallbackContext) + (kinds : List ScalarLoadKind) (argWords : List Nat) : Prop := + dispatchWordsMatch kinds argWords (ctx.calldata.take argWords.length) = true ∧ + Verity.Core.Uint256.ofNat (4 + 32 * argWords.length) ≤ ctx.calldataSize + +instance (ctx : CallbackContext) (kinds : List ScalarLoadKind) (argWords : List Nat) : + Decidable (dispatchCalldataMatchesKinds ctx kinds argWords) := by + dsimp [dispatchCalldataMatchesKinds] + infer_instance + instance (ctx : CallbackContext) (argWords : List Nat) : Decidable (dispatchCalldataMatches ctx argWords) := by dsimp [dispatchCalldataMatches] @@ -246,6 +317,7 @@ def withCallbackContext (ctx : CallbackContext) (world : Verity.ContractState) : selfBalance := world.selfBalance + ctx.msgValue calldataSize := ctx.calldataSize calldata := ctx.calldata + selector := ctx.selector memory := fun _ => 0 returndata := [] } @@ -256,6 +328,7 @@ def restoreCallbackContext (outer callbackResult : Verity.ContractState) : msgValue := outer.msgValue calldataSize := outer.calldataSize calldata := outer.calldata + selector := outer.selector memory := outer.memory returndata := outer.returndata } @@ -308,6 +381,10 @@ def callbackContractTransition (ctx : CallbackContext) (world : Verity.ContractState) : (withCallbackContext ctx world).calldataSize = ctx.calldataSize := rfl +@[simp] theorem withCallbackContext_selector (ctx : CallbackContext) + (world : Verity.ContractState) : + (withCallbackContext ctx world).selector = ctx.selector := rfl + @[simp] theorem withCallbackContext_selfBalance (ctx : CallbackContext) (world : Verity.ContractState) : (withCallbackContext ctx world).selfBalance = world.selfBalance + ctx.msgValue := rfl @@ -340,6 +417,11 @@ def callbackContractTransition (ctx : CallbackContext) (outer : Verity.ContractState) : (callbackTransition ctx entrypoint outer).calldataSize = outer.calldataSize := rfl +@[simp] theorem callbackTransition_restores_selector (ctx : CallbackContext) + (entrypoint : Verity.ContractState → Verity.ContractState) + (outer : Verity.ContractState) : + (callbackTransition ctx entrypoint outer).selector = outer.selector := rfl + @[simp] theorem callbackTransition_restores_memory (ctx : CallbackContext) (entrypoint : Verity.ContractState → Verity.ContractState) (outer : Verity.ContractState) : diff --git a/Verity/Macro/Translate.lean b/Verity/Macro/Translate.lean index ad3c3622b..0bfe3ea88 100644 --- a/Verity/Macro/Translate.lean +++ b/Verity/Macro/Translate.lean @@ -2511,6 +2511,42 @@ def translatedBodyContainsExternalCall | _ => throwErrorAt bodyTerm "failed to reduce the translated external-call predicate" +private partial def syntaxContainsCalldataRead (stx : Syntax) : Bool := + match stx with + | `(term| calldatasize) | `(term| calldataload $_) => true + | _ => stx.getArgs.any syntaxContainsCalldataRead + +def translatedBodyContainsCalldataRead + (stmtTerms : Array Term) : CommandElabM Bool := do + if stmtTerms.any (fun t => syntaxContainsCalldataRead t.raw) then + return true + let bodyTerm : Term ← `([ $[$stmtTerms],* ]) + liftTermElabM do + let predicate : Term ← + `($(bodyTerm).any (fun s => + Compiler.CompilationModel.Stmt.anyDeep + (fun + | .letVar _ (.calldatasize) => true + | .letVar _ (.calldataload _) => true + | .assignVar _ (.calldatasize) => true + | .assignVar _ (.calldataload _) => true + | .setStorage _ (.calldatasize) => true + | .setStorage _ (.calldataload _) => true + | .ite (.calldatasize) _ _ => true + | .ite (.calldataload _) _ _ => true + | .return (.calldatasize) => true + | .return (.calldataload _) => true + | _ => false) + s)) + let expr ← Lean.Elab.Term.elabTermEnsuringType predicate (mkConst ``Bool) + match ← Lean.Meta.withTransparency .all (Lean.Meta.whnf expr) with + | .const ``Bool.true _ => pure true + | .const ``Bool.false _ => pure false + | _ => + -- Syntax walk already ran; if the model predicate does not reduce, + -- keep the conservative syntax result (false here). + pure false + private partial def syntaxCallsAnyHelper (helperNames : Array String) (stx : Syntax) : CommandElabM Bool := do match stx with @@ -2762,8 +2798,10 @@ private def threadHelperApp? mkSuffixedIdent helper.ident "_unguarded" else pure helper.ident - if matchesExactHelper helper && adversarialHelpers.any (fun candidate => - functionSignatureKey candidate == functionSignatureKey helper) then + if matchesExactHelper helper && + (adversarialHelpers.any (fun candidate => + functionSignatureKey candidate == functionSignatureKey helper) || + registryOnly) then some <$> helperCallWithAdv target args adv else if helper.nonReentrantLock.isSome && helper.reentrancyTrusted then some <$> helperCall target args @@ -5986,6 +6024,59 @@ def mkStructExternalArgInstanceCommandPublic (decl : StructDecl) : CommandElabM ([ $[$encodedFields],* ] : List (List _root_.Verity.Uint256))) +/-- Compile-time DispatchVal from a declared `ValueType`, so `FixedArray` +encodes as a tuple (no `T[]` length) and `Tuple` flattens rather than +following the right-nested Lean product instance. -/ +private def scalarLoadKindTerm : ValueType → CommandElabM Term + | .bool => `(Compiler.CompilationModel.DenoteExternalCalls.ScalarLoadKind.bool) + | .uint8 => `(Compiler.CompilationModel.DenoteExternalCalls.ScalarLoadKind.uint8) + | .uint16 => `(Compiler.CompilationModel.DenoteExternalCalls.ScalarLoadKind.uint16) + | .uintN bits => + `(Compiler.CompilationModel.DenoteExternalCalls.ScalarLoadKind.uintN $(natTerm bits)) + | .address => `(Compiler.CompilationModel.DenoteExternalCalls.ScalarLoadKind.address) + | .bytesN bytes => + `(Compiler.CompilationModel.DenoteExternalCalls.ScalarLoadKind.bytesN $(natTerm bytes)) + | _ => `(Compiler.CompilationModel.DenoteExternalCalls.ScalarLoadKind.identity) + +private partial def dispatchValTermForType (ty : ValueType) (value : Term) : CommandElabM Term := + match ty with + | .fixedArray elemTy size => do + let mut elems : Array Term := #[] + for i in [:size] do + let elem ← `(term| Array.getD $value $(natTerm i) default) + elems := elems.push (← dispatchValTermForType elemTy elem) + if elems.isEmpty then + `(term| Compiler.CompilationModel.DenoteExternalCalls.dispatchFixedArray + ([] : List Compiler.CompilationModel.DenoteExternalCalls.DispatchVal)) + else + `(term| Compiler.CompilationModel.DenoteExternalCalls.dispatchFixedArray + ([ $[$elems],* ] : List Compiler.CompilationModel.DenoteExternalCalls.DispatchVal)) + | .tuple elemTys => do + let mut elems : Array Term := #[] + let mut rest : Term := value + let mut idx : Nat := 0 + for elemTy in elemTys do + let elem ← + if idx + 1 == elemTys.length then + pure rest + else do + let head ← `(term| Prod.fst $rest) + rest ← `(term| Prod.snd $rest) + pure head + elems := elems.push (← dispatchValTermForType elemTy elem) + idx := idx + 1 + `(term| Compiler.CompilationModel.DenoteExternalCalls.dispatchFlatTuple + (Compiler.CompilationModel.DenoteExternalCalls.DispatchVal.tuple + ([ $[$elems],* ] : List Compiler.CompilationModel.DenoteExternalCalls.DispatchVal))) + | .array elemTy => do + `(term| Compiler.CompilationModel.DenoteExternalCalls.DispatchVal.array + (($value).toList.map (fun x => + Compiler.CompilationModel.DenoteExternalCalls.ToDispatchVal.toDispatchVal + (x : $(← contractValueTypeTerm elemTy))))) + | _ => + `(term| Compiler.CompilationModel.DenoteExternalCalls.ToDispatchVal.toDispatchVal + $value) + /-- Compiled-dispatch ABI encoding of a named struct: the tuple of its fields, matching `genParamLoads` / `abiEncodeDispatchArgs`. -/ def mkStructToDispatchValInstanceCommandPublic (decl : StructDecl) : CommandElabM Cmd := do @@ -6622,6 +6713,7 @@ def mkFunctionCommandsPublic let directlyOpensReentrancyWindow ← translatedBodyOpensReentrancyWindow stmtTerms let mut adversarialHelpers : Array FunctionDecl := #[] let mut windowHelpers : Array FunctionDecl := #[] + let mut calldataHelpers : Array FunctionDecl := #[] let mut translatedHelpers : Array (FunctionDecl × FunctionDecl) := #[] for helper in functions do let helperModel ← @@ -6639,6 +6731,9 @@ def mkFunctionCommandsPublic adversarialHelpers := adversarialHelpers.push helper if ← translatedBodyOpensReentrancyWindow helperStmtTerms then windowHelpers := windowHelpers.push helper + if syntaxContainsCalldataRead helperModel.body.raw || + (← translatedBodyContainsCalldataRead helperStmtTerms) then + calldataHelpers := calldataHelpers.push helper -- Reentrancy-window capability is transitive across internal helpers. Iterate to a -- fixed point so every caller in a multi-hop helper chain receives and forwards -- the same adversary instead of silently falling back to the stub. @@ -6666,6 +6761,23 @@ def mkFunctionCommandsPublic grew := true if !grew then break + for _ in [:functions.size] do + let calldataNames := calldataHelpers.map (·.name) + let mut grew := false + for (helper, helperModel) in translatedHelpers do + let callsCalldata ← syntaxCallsAnyHelper calldataNames helperModel.body.raw + if !calldataHelpers.any (fun candidate => + functionSignatureKey candidate == functionSignatureKey helper) && + callsCalldata then + calldataHelpers := calldataHelpers.push helper + grew := true + if !grew then + break + let mut registryOnlyHelpers : Array FunctionDecl := adversarialHelpers + for helper in calldataHelpers do + if !registryOnlyHelpers.any (fun candidate => + functionSignatureKey candidate == functionSignatureKey helper) then + registryOnlyHelpers := registryOnlyHelpers.push helper let windowNames := windowHelpers.map (·.name) let callsWindow ← syntaxCallsAnyHelper windowNames modelFn.body.raw let opensReentrancyWindow := directlyOpensReentrancyWindow || callsWindow @@ -6687,9 +6799,10 @@ def mkFunctionCommandsPublic -- window-opening helpers, to `_registry` / `_registry_unguarded`. Restricting -- the suffix to non-window helpers let `entry_registry` call public `hop`, -- which then used stub-only nested view helpers and under-approximated the - -- compiled callee-controlled ECM. + -- compiled callee-controlled ECM. Calldata-reading helpers are also + -- registry-only so they observe `calldataloadLive` rather than the stub. let registryExecutableBody := ⟨← threadAdversaryThroughExecutableSyntax fields constDecls immutableDecls - externalDecls functions adversarialHelpers adversarialHelpers fn.params #[] + externalDecls functions adversarialHelpers functions fn.params #[] (⟨advIdent.raw⟩ : Term) fnExecutableBody.raw (linkedContracts := linkedContracts) (registryMode := true)⟩ let mut extraExecutableCmds : Array Cmd := #[] @@ -6796,19 +6909,35 @@ def mkFunctionCommandsPublic $context:ident ∧ $registryBody) else if fn.name != "fallback" then let mut dispatchValTerms : Array Term := #[] - for (paramIdent, _) in registryParams do + let mut kindTerms : Array Term := #[] + for (param, (paramIdent, _)) in fn.params.zip registryParams do dispatchValTerms := dispatchValTerms.push - (← `(Compiler.CompilationModel.DenoteExternalCalls.ToDispatchVal.toDispatchVal - $paramIdent:ident)) + (← dispatchValTermForType param.ty ⟨paramIdent.raw⟩) + kindTerms := kindTerms.push (← scalarLoadKindTerm param.ty) let argWordsTerm ← if dispatchValTerms.isEmpty then `( ([] : List Nat) ) else `(Compiler.CompilationModel.DenoteExternalCalls.abiEncodeDispatchArgs ([ $[$dispatchValTerms],* ] : List Compiler.CompilationModel.DenoteExternalCalls.DispatchVal)) - registryBody ← - `(Compiler.CompilationModel.DenoteExternalCalls.dispatchCalldataMatches - $context:ident $argWordsTerm ∧ $registryBody) + let usesScalarNorm := fn.params.any fun p => + match p.ty with + | .bool | .uint8 | .uint16 | .uintN _ | .address | .bytesN _ => true + | _ => false + if usesScalarNorm then + let kindsTerm ← + if kindTerms.isEmpty then + `( ([] : List Compiler.CompilationModel.DenoteExternalCalls.ScalarLoadKind) ) + else + `( ([ $[$kindTerms],* ] : + List Compiler.CompilationModel.DenoteExternalCalls.ScalarLoadKind) ) + registryBody ← + `(Compiler.CompilationModel.DenoteExternalCalls.dispatchCalldataMatchesKinds + $context:ident $kindsTerm $argWordsTerm ∧ $registryBody) + else + registryBody ← + `(Compiler.CompilationModel.DenoteExternalCalls.dispatchCalldataMatches + $context:ident $argWordsTerm ∧ $registryBody) for (paramIdent, paramTy) in registryParams.reverse do registryBody ← `(∃ $paramIdent:ident : $paramTy, $registryBody) registryBody ← diff --git a/Verity/Proofs/Model/GeneratedEntrypointRegistry.lean b/Verity/Proofs/Model/GeneratedEntrypointRegistry.lean index d7e79ff99..7ab367c40 100644 --- a/Verity/Proofs/Model/GeneratedEntrypointRegistry.lean +++ b/Verity/Proofs/Model/GeneratedEntrypointRegistry.lean @@ -402,6 +402,150 @@ theorem setFromCalldata_registered_live end RegistryLiveCalldata +verity_contract RegistryLiveSelector where + storage + last : Uint256 := slot 0 + + function setFromSelector (_unused : Uint256) + local_obligations [manual_low_level_refinement := assumed + "Fixture reads compiled-dispatch selector via calldataload 0."] + : Unit := do + let loaded := calldataload 0 + setStorage last loaded + return () + +namespace RegistryLiveSelector + +def selCtx : CallbackContext where + sender := 0 + msgValue := 0 + calldataSize := Verity.Core.Uint256.ofNat 4 + calldata := [] + selector := 0xa9059cbb + +def publicWritesOffset : Bool := + match (setFromSelector (0 : Uint256)).run Verity.defaultState with + | .success _ s => s.storage 0 == 0 + | _ => false + +example : publicWritesOffset = true := by decide + +def liveWritesSelectorWord : Bool := + let s := callbackContractTransition selCtx + (setFromSelector_registry + (Contracts.ExecutableCallContext.ofAdversary AdversaryModel.stub) 0) + Verity.defaultState + s.storage 0 == Compiler.CompilationModel.Denote.selectorWord 0xa9059cbb + +example : liveWritesSelectorWord = true := by decide + +end RegistryLiveSelector + +/-! P1: FixedArray vs Array encoding; flattened 3-member tuples. -/ +example : + DispatchVal.isDynamic + (dispatchFixedArray + [ToDispatchVal.toDispatchVal (1 : Uint256), + ToDispatchVal.toDispatchVal (2 : Uint256)]) = false := + rfl + +example : + (dispatchFixedArray + [ToDispatchVal.toDispatchVal (1 : Uint256), + ToDispatchVal.toDispatchVal (2 : Uint256)]).payloadWords = [1, 2] := + rfl + +example : + dispatchArgsAllWords + [ToDispatchVal.toDispatchVal (#[(1 : Uint256), (2 : Uint256)] : Array Uint256)] = false := + rfl + +example : + (abiEncodeDispatchArgs + [ToDispatchVal.toDispatchVal (#[(1 : Uint256), (2 : Uint256)] : Array Uint256)]).head? = + some 32 := + rfl + +example : + dispatchArgsAllWords + [ToDispatchVal.toDispatchVal + ((1 : Uint256), ("ab", (3 : Uint256)))] = false := + rfl + +example : + dispatchArgsAllWords + [dispatchFlatTuple + (DispatchVal.tuple + [ToDispatchVal.toDispatchVal (1 : Uint256), + ToDispatchVal.toDispatchVal ("ab" : String), + ToDispatchVal.toDispatchVal (3 : Uint256)])] = false := + rfl + +/-! P1: genScalarLoad-normalized noncanonical scalar words still match. -/ +example : dispatchCalldataMatchesKinds + { sender := 0, msgValue := 0, + calldataSize := Verity.Core.Uint256.ofNat 36, calldata := [2] } + [.bool] + (abiEncodeDispatchArgs [ToDispatchVal.toDispatchVal true]) := by decide + +example : dispatchCalldataMatchesKinds + { sender := 0, msgValue := 0, + calldataSize := Verity.Core.Uint256.ofNat 36, calldata := [256] } + [.uint8] + (abiEncodeDispatchArgs [ToDispatchVal.toDispatchVal (0 : Verity.Core.UIntN 8)]) := by decide + +example : dispatchCalldataMatchesKinds + { sender := 0, msgValue := 0, + calldataSize := Verity.Core.Uint256.ofNat 36, + calldata := [Compiler.Constants.addressMask + 1 + 7] } + [.address] + (abiEncodeDispatchArgs [ToDispatchVal.toDispatchVal (7 : Address)]) := by decide + +/-! P1: calldata-reading helpers are routed through *_registry. -/ +verity_contract RegistryCalldataHelperRouting where + storage + last : Uint256 := slot 0 + + function loadArg (_unused : Uint256) + local_obligations [manual_low_level_refinement := assumed + "Helper reads compiled-dispatch calldata via calldataload 4."] + : Uint256 := do + return calldataload 4 + + function setFromHelper (_value : Uint256) + local_obligations [manual_low_level_refinement := assumed + "Entrypoint delegates calldata load to an internal helper."] + : Unit := do + let loaded ← loadArg 0 + setStorage last loaded + return () + +namespace RegistryCalldataHelperRouting + +def helperCtx (value : Uint256) : CallbackContext where + sender := 0 + msgValue := 0 + calldataSize := Verity.Core.Uint256.ofNat 36 + calldata := [value.val] + +def publicHelperWritesStub : Bool := + match (setFromHelper (7 : Uint256)).run Verity.defaultState with + | .success _ s => s.storage 0 == 4 + | _ => false + +example : publicHelperWritesStub = true := by decide + +def registryHelperWritesLive : Bool := + let s := callbackContractTransition (helperCtx 7) + (setFromHelper_registry + (Contracts.ExecutableCallContext.ofAdversary AdversaryModel.stub) 7) + Verity.defaultState + s.storage 0 == 7 + +example : registryHelperWritesLive = true := by decide + +end RegistryCalldataHelperRouting + /-! Regression: public Solidity self-calls still hit the nonReentrant tload prologue. Bound hops must keep the guarded registry path, not `*_unguarded`. -/ verity_contract RegistrySelfCallGuard where From 6c16a51d259055e28628255019c8606347b6be10 Mon Sep 17 00:00:00 2001 From: "github-actions[bot]" Date: Mon, 21 Sep 2026 04:21:53 +0100 Subject: [PATCH 47/50] chore: auto-refresh derived artifacts --- artifacts/trust_surface_report.json | 2 +- artifacts/verification_status.json | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/artifacts/trust_surface_report.json b/artifacts/trust_surface_report.json index b390cc7bf..0e2bd2f21 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": 179 + "partial def": 181 }, "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 9fe885b2b..972dbff14 100644 --- a/artifacts/verification_status.json +++ b/artifacts/verification_status.json @@ -1,6 +1,6 @@ { "codebase": { - "core_lines": 2257, + "core_lines": 2260, "example_contracts": 20 }, "proofs": { From bcf0eeffd262facbba4f6a3dbee8f0c0b11508f1 Mon Sep 17 00:00:00 2001 From: Thomas Marchand Date: Mon, 21 Sep 2026 08:22:57 +0000 Subject: [PATCH 48/50] fix(ci): trigger Verify proofs on artifacts/* auto-refresh GitHub path filters treat artifacts/** as nested-only, so an artifacts-only bot refresh (6c16a51d, run 35557277208) completed action_required with zero jobs. Add artifacts/* alongside artifacts/** so the required check still runs. Same pattern as PrintAxioms.lean in ef0321d6. --- .github/workflows/verify.yml | 2 ++ AUDIT.md | 2 +- scripts/verify_sync_spec.json | 1 + scripts/verify_sync_spec_source.py | 1 + 4 files changed, 5 insertions(+), 1 deletion(-) diff --git a/.github/workflows/verify.yml b/.github/workflows/verify.yml index 45f6370b8..5809643d8 100644 --- a/.github/workflows/verify.yml +++ b/.github/workflows/verify.yml @@ -11,6 +11,7 @@ on: - '.github/workflows/**' - '.github/workflows/verify.yml' - 'artifacts/**' + - 'artifacts/*' - 'Verity/**' - 'Verity.lean' - 'Compiler/**' @@ -40,6 +41,7 @@ on: - '.github/workflows/**' - '.github/workflows/verify.yml' - 'artifacts/**' + - 'artifacts/*' - 'Verity/**' - 'Verity.lean' - 'Compiler/**' diff --git a/AUDIT.md b/AUDIT.md index d2da61776..1b0579ea8 100644 --- a/AUDIT.md +++ b/AUDIT.md @@ -620,7 +620,7 @@ sibling entrypoint. | `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`. `PrintAxioms.lean` is a Verify proofs path-filter so bot auto-refresh of that file still runs `checks`. The checks job regenerates `artifacts/trust_surface_report.json` with the other derived artifacts so `make check` cannot fail on a stale trust-surface report after auto-refresh. | +| `PrintAxioms.lean` / generated axiom report | Axiom dependency visibility | `python3 scripts/generate_print_axioms.py --check` and `lake build PrintAxioms`. `PrintAxioms.lean` is a Verify proofs path-filter so bot auto-refresh of that file still runs `checks`. The checks job regenerates `artifacts/trust_surface_report.json` with the other derived artifacts so `make check` cannot fail on a stale trust-surface report after auto-refresh. Direct files under `artifacts/` also use the `artifacts/*` trigger (GitHub `artifacts/**` does not match them), so an artifacts-only auto-refresh cannot skip the required Verify proofs check (run 35557277208). | | `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` | diff --git a/scripts/verify_sync_spec.json b/scripts/verify_sync_spec.json index bab7906aa..4c9e5122a 100644 --- a/scripts/verify_sync_spec.json +++ b/scripts/verify_sync_spec.json @@ -3,6 +3,7 @@ ".github/workflows/**", ".github/ISSUE_TEMPLATE/**", "artifacts/**", + "artifacts/*", "docs/**", "docs-site/**", "Makefile", diff --git a/scripts/verify_sync_spec_source.py b/scripts/verify_sync_spec_source.py index fa34e47b3..285704ff8 100644 --- a/scripts/verify_sync_spec_source.py +++ b/scripts/verify_sync_spec_source.py @@ -12,6 +12,7 @@ SPEC = {'check_only_paths': ['.github/workflows/**', '.github/ISSUE_TEMPLATE/**', 'artifacts/**', + 'artifacts/*', 'docs/**', 'docs-site/**', 'Makefile', From b25e90298a844e71bd8cbc5b5544cb4295627c8a Mon Sep 17 00:00:00 2001 From: Thomas Marchand Date: Tue, 29 Sep 2026 13:35:39 +0200 Subject: [PATCH 49/50] fix(macro): thread new threadHelperApp? arguments through adversary-threaded helper calls (merge of main into #2406) Co-Authored-By: Claude Opus 5.5 --- Verity/Macro/Translate.lean | 7 +++++-- 1 file changed, 5 insertions(+), 2 deletions(-) diff --git a/Verity/Macro/Translate.lean b/Verity/Macro/Translate.lean index f9db67768..785f7a495 100644 --- a/Verity/Macro/Translate.lean +++ b/Verity/Macro/Translate.lean @@ -3835,9 +3835,12 @@ private partial def threadAdversaryThroughExecutableSyntax -- form above; everything else keeps the generic hoisting path. let helperApp? ← match rhs with | `(term| $fn:ident $args:term*) => - threadHelperApp? adversarialHelpers fn (args.map fun arg => (⟨arg.raw⟩ : Term)) adv + threadHelperApp? fields constDecls immutableDecls externalDecls + helpers adversarialHelpers registryOnlyHelpers params locals fn + (args.map fun arg => (⟨arg.raw⟩ : Term)) adv | `(term| $fn:ident($[$args:term],*)) => - threadHelperApp? adversarialHelpers fn args adv + threadHelperApp? fields constDecls immutableDecls externalDecls + helpers adversarialHelpers registryOnlyHelpers params locals fn args adv | _ => pure none match helperApp? with | some app => From ec3cdc3b574228c2a98c8c158a1a0834be099cc7 Mon Sep 17 00:00:00 2001 From: "github-actions[bot]" Date: Tue, 29 Sep 2026 13:51:30 +0200 Subject: [PATCH 50/50] chore: auto-refresh derived artifacts --- PrintAxioms.lean | 2 +- artifacts/trust_surface_report.json | 2 +- artifacts/verification_status.json | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/PrintAxioms.lean b/PrintAxioms.lean index f5fbb31a4..28d1c4283 100644 --- a/PrintAxioms.lean +++ b/PrintAxioms.lean @@ -7631,4 +7631,4 @@ end Verity.AxiomAudit Compiler.Proofs.YulGeneration.YulTransaction.ofIR_args ] --- Total: 7036 theorems/lemmas (5023 public, 2013 private, 0 sorry'd) +-- Total: 7045 theorems/lemmas (5032 public, 2013 private, 0 sorry'd) diff --git a/artifacts/trust_surface_report.json b/artifacts/trust_surface_report.json index e4341d80c..71da346cf 100644 --- a/artifacts/trust_surface_report.json +++ b/artifacts/trust_surface_report.json @@ -169,7 +169,7 @@ "mechanisms": { "@[implemented_by": 4, "native_decide": 479, - "partial def": 197 + "partial def": 200 }, "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/artifacts/verification_status.json b/artifacts/verification_status.json index 015a9839c..0032bfd16 100644 --- a/artifacts/verification_status.json +++ b/artifacts/verification_status.json @@ -1,6 +1,6 @@ { "codebase": { - "core_lines": 2810, + "core_lines": 2813, "example_contracts": 19 }, "proofs": {