diff --git a/.gitconfig b/.gitconfig deleted file mode 100644 index e69de29bb2..0000000000 diff --git a/.github/CODEOWNERS b/.github/CODEOWNERS index 72ea98aca1..6d1ba30003 100644 --- a/.github/CODEOWNERS +++ b/.github/CODEOWNERS @@ -7,8 +7,8 @@ * @Th0rgal # Axioms and trust assumptions — any change weakens guarantees -AXIOMS.md @Th0rgal -TRUST_ASSUMPTIONS.md @Th0rgal +docs/AXIOMS.md @Th0rgal +docs/TRUST_ASSUMPTIONS.md @Th0rgal # Lean proof files — ensure no sorry regressions Verity/Proofs/ @Th0rgal @@ -20,10 +20,8 @@ Verity/Specs/ @Th0rgal Verity/Core.lean @Th0rgal # Compiler trusted code — bugs here break soundness -Compiler/Specs.lean @Th0rgal Compiler/IR.lean @Th0rgal Compiler/Codegen.lean @Th0rgal -Compiler/Interpreter.lean @Th0rgal # CI pipeline — governs what gets checked .github/workflows/ @Th0rgal diff --git a/.github/ocr/rules.json b/.github/ocr/rules.json index 3ae865edc8..f78dd47225 100644 --- a/.github/ocr/rules.json +++ b/.github/ocr/rules.json @@ -1,7 +1,7 @@ { "rules": [ { - "path": "{AUDIT.md,TRUST_ASSUMPTIONS.md,AXIOMS.md}", + "path": "docs/{TRUST_ASSUMPTIONS.md,AXIOMS.md}", "rule": "Treat trust-boundary documentation changes as high-risk. Check whether a code/proof change weakens, broadens, or silently depends on assumptions, axioms, admitted facts, trusted external semantics, compiler equivalence, or audit exclusions. Flag any mismatch between these docs and changed Lean/Solidity/Yul/Cairo code. Prefer concrete line-linked findings; do not comment on wording unless it changes the trust model.", "merge_system_rule": true }, @@ -50,9 +50,8 @@ "**/*.sol", "**/*.yul", "**/*.cairo", - "AUDIT.md", - "TRUST_ASSUMPTIONS.md", - "AXIOMS.md", + "docs/TRUST_ASSUMPTIONS.md", + "docs/AXIOMS.md", "README.md", "docs/**/*.md", ".github/**/*" diff --git a/.github/scripts/ocr-router.js b/.github/scripts/ocr-router.js index 981765f2d4..bf30d84fa9 100644 --- a/.github/scripts/ocr-router.js +++ b/.github/scripts/ocr-router.js @@ -1345,7 +1345,7 @@ function countCategories(included, supportedFiles) { function categorize(filePath) { const base = path.posix.basename(filePath); if (filePath.endsWith('.lean')) return 'lean'; - if (['AUDIT.md', 'TRUST_ASSUMPTIONS.md', 'AXIOMS.md'].includes(base)) return 'trust-doc'; + if (['TRUST_ASSUMPTIONS.md', 'AXIOMS.md'].includes(base)) return 'trust-doc'; if (filePath.startsWith('.github/')) return 'workflow-script'; if (/\.(sol|yul|cairo)$/.test(filePath)) return 'contract'; if (filePath === 'README.md' || (filePath.startsWith('docs/') && filePath.endsWith('.md'))) return 'doc'; @@ -1358,7 +1358,7 @@ function isSupported(filePath) { const base = path.posix.basename(filePath); return filePath.endsWith('.lean') || /\.(sol|yul|cairo)$/.test(filePath) || - ['AUDIT.md', 'TRUST_ASSUMPTIONS.md', 'AXIOMS.md', 'README.md'].includes(base) || + ['TRUST_ASSUMPTIONS.md', 'AXIOMS.md', 'README.md'].includes(base) || (filePath.startsWith('docs/') && filePath.endsWith('.md')) || filePath.startsWith('.github/') || /\.(py|sh|bash|js|ts|json|ya?ml|toml)$/.test(filePath); diff --git a/.github/workflows/verify.yml b/.github/workflows/verify.yml index bbf17e98ea..d50d08ae85 100644 --- a/.github/workflows/verify.yml +++ b/.github/workflows/verify.yml @@ -28,9 +28,7 @@ on: - 'lean-toolchain' - 'foundry.toml' - 'Makefile' - - 'AXIOMS.md' - 'README.md' - - 'TRUST_ASSUMPTIONS.md' pull_request: types: [opened, synchronize, reopened, closed] paths: @@ -56,9 +54,7 @@ on: - 'lean-toolchain' - 'foundry.toml' - 'Makefile' - - 'AXIOMS.md' - 'README.md' - - 'TRUST_ASSUMPTIONS.md' workflow_dispatch: inputs: clean_build: @@ -231,6 +227,11 @@ jobs: - 'packages/**' - 'lakefile.lean' - 'lake-manifest.json' + - 'scripts/solidity_differential/**' + - 'scripts/solidity_import_differential.py' + - 'scripts/check_solidity_differential.sh' + - 'scripts/solidity_import_mutations.py' + - 'scripts/setup_solc_import.py' - 'lean-toolchain' compiler: - '.github/actions/**' @@ -255,6 +256,11 @@ jobs: - 'scripts/property_utils.py' - 'lakefile.lean' - 'lake-manifest.json' + - 'scripts/solidity_differential/**' + - 'scripts/solidity_import_differential.py' + - 'scripts/check_solidity_differential.sh' + - 'scripts/solidity_import_mutations.py' + - 'scripts/setup_solc_import.py' - 'lean-toolchain' - 'foundry.toml' - name: Resolve effective change flags @@ -391,11 +397,6 @@ jobs: disable-lake-cache-restore: ${{ env.VERIFY_DISABLE_LAKE_CACHE_RESTORE }} cache-primary-key: lake-${{ runner.os }}-${{ hashFiles('lean-toolchain') }}-${{ hashFiles('lakefile.lean') }}-${{ hashFiles('lake-manifest.json') }}-${{ github.run_id }} - - name: Setup pinned solc for Lean Solidity importer - uses: ./.github/actions/setup-solc - with: - destination: .lake/solidity-import/solc - - name: Rebuild cached local Lean modules run: | rm -rf .lake/build/lib/lean/Verity .lake/build/ir/Verity @@ -424,6 +425,12 @@ jobs: exit "${build_status}" fi + - name: Check Solidity slice imports and mutation regressions + run: | + python3 scripts/setup_solc_import.py + lake build SolidityImportSmoke + python3 scripts/solidity_import_mutations.py + - name: Check storage layout audit artifacts (#1897) run: make verify-storage-layout @@ -1228,6 +1235,38 @@ jobs: if: steps.local-restore-compiler-workspace.outcome != 'success' run: tar -xf lean-workspace-compiler-build.tar + - name: Install Foundry for Solidity slice comparisons + uses: foundry-rs/foundry-toolchain@v1 + with: + version: v1.5.0 + + - name: Compare Solidity, Denote and compiled models + timeout-minutes: 10 + run: | + scripts/check_solidity_differential.sh --config Contracts/SolidityImportSmoke/differential.json --output .lake/import-differential/smoke --cases 128 --seed 2438 + scripts/check_solidity_differential.sh --config Contracts/SolidityImportSmoke/differential-loss.json --output .lake/import-differential/loss --cases 64 --seed 2438 + scripts/check_solidity_differential.sh --programs 5 --cases 16 --seed 2438 --output .lake/import-differential/programs + + - name: Extended Solidity slice campaigns + if: github.event_name == 'schedule' || github.event_name == 'workflow_dispatch' + timeout-minutes: 60 + run: | + scripts/check_solidity_differential.sh --config Contracts/SolidityImportSmoke/differential.json --output .lake/import-differential/nightly-inputs --cases 2048 --seed "$GITHUB_RUN_NUMBER" + scripts/check_solidity_differential.sh --programs 20 --cases 64 --seed "$GITHUB_RUN_NUMBER" --output .lake/import-differential/nightly-programs + scripts/check_solidity_differential.sh --mutations --output .lake/import-differential/mutants + + - name: Archive Solidity slice comparison evidence + if: always() + uses: actions/upload-artifact@v7 + with: + name: solidity-import-differential + path: | + .lake/import-differential/** + !.lake/import-differential/**/.lake/build/** + !.lake/import-differential/**/.lake/packages/** + include-hidden-files: true + retention-days: 14 + - name: Build compiler CLI regression executable run: stdbuf -oL -eL lake build compiler-main-test diff --git a/.profile b/.profile deleted file mode 100644 index f45f3b2193..0000000000 --- a/.profile +++ /dev/null @@ -1,5 +0,0 @@ - -export PATH="$HOME/.elan/bin:$PATH" - -export NVM_DIR="$HOME/.config/nvm" -[ -s "$NVM_DIR/nvm.sh" ] && \. "$NVM_DIR/nvm.sh" # This loads nvm diff --git a/AUDIT.md b/AUDIT.md deleted file mode 100644 index 80e7d9f150..0000000000 --- a/AUDIT.md +++ /dev/null @@ -1,858 +0,0 @@ -# Audit Registry - -This file records trust-boundary changes and the evidence that keeps them -reviewable. Keep it synchronized with `TRUST_ASSUMPTIONS.md` and `AXIOMS.md` -whenever semantics, trusted components, generated audit artifacts, or CI -boundary checks change. - -## Proof-only Solidity Vault POC - -The focused suite probes unknown, wrong-typed, and missing AST fields (including -documentation metadata), invalid source spans, and malformed storage layout -through synthetic compiler-output mutations, plus unsupported source constructs, -contract `layout at`, -registered-source symlink escape, and Lean importer digest sensitivity. It also -checks safe transparent declarations, duplicate aliases, a deliberately -malformed late declaration and complete registration rollback (including the -named storage view), plus the pinned compiler's checksum. Named-storage checks -cover `v.totalAssets` dot notation and `#print view`, a -declaration-reorder mutation that moves solc slots while every proof still -builds, a state-variable rename that makes `Spec.lean` fail to elaborate, and -Solidity variables named `Storage` or `step` rejected with a source position. -Behaviour mutations must break both the `*_success_spec` theorem and the -`*_meets_spec` theorem of the affected entry point, three `Spec.lean` mutations -that weaken a promise must fail inside both corresponding theorems, and a new -`mint` entry point must break `solvent_invariant`. The `spec_named_storage` -lean_lint rule (in `make check`) rejects every raw `ContractState` accessor -(the list is read from `Verity/Core.lean`), raw storage fields, direct -`ContractState` mentions, positional projections, and `knownAddresses` in -opted-in spec files. The digest scope is documented in `TRUST_ASSUMPTIONS.md`; it is not a transitive build identity. - -The importer accepts the official 0.8.33 linux-amd64 and macosx-amd64 -builds published on `binaries.soliditylang.org` (the same lists `solc-select` -uses). Checksum verification uses `/usr/bin/sha256sum` on Linux and -`/usr/bin/shasum` on macOS against that committed allowlist. `sourceDigest` -hashes source, standard-json, importer text, and `0.8.33+commit.64118f21`, not -the host binary. Setup and `make check-solc-published` may fetch `list.json`; -Lake does not. Linux CI still installs and runs the linux-amd64 artifact. - -Evidence command: -`python3 Contracts/VaultFromSolidity/Importer/scripts/solidity_importer_test.py` -(after `lake build VaultFromSolidity` and installation of the pinned compiler). -The focused runner builds and audits the imported execution proofs, changes -accepted deposit/getter behavior while preserving source mtime and requires old -proofs to fail at both the success and spec layer, weakens the named spec -and requires both corresponding theorems to fail, rejects unsupported source, -checks unchanged artifacts, and -exercises Lean-importer and compiler content invalidation. Mutations occur only -in disposable copies. This is local acceptance evidence, not a new CI job, -bytecode/runtime test, or proof of translation correctness. - -The complete example surface lives under `Contracts/VaultFromSolidity`: Solidity -source, Lean importer, specification, execution proofs and focused acceptance -tests. It is independent of the handwritten `Contracts/Vault` example. The S1 -inheritance slice adds `Contracts/SolidityImportSmoke/Inheritance` with the same -shape (source, import command, named-storage spec, proofs, focused Python -suite). The S2 modifiers/structs slice adds -`Contracts/SolidityImportSmoke/Modifiers` and -`Contracts/SolidityImportSmoke/Structs` with the same shape. No -Python frontend, custom serialized IR, generated Lean source, or bytecode is in -the translation path: the accepted Solidity subset is the kernel-checked -inductive in `Importer/Syntax.lean` and `Importer/Semantics.lean` is its single -meaning. Trust and axiom scope are recorded in -`TRUST_ASSUMPTIONS.md` and `AXIOMS.md`. - -Evidence command for the inheritance slice: -`python3 Contracts/SolidityImportSmoke/Inheritance/scripts/inheritance_test.py` -(after `lake build SolidityImportSmokeInheritance`). -Evidence commands for the modifiers/structs slice: -`python3 Contracts/SolidityImportSmoke/Modifiers/scripts/modifiers_test.py` -(after `lake build SolidityImportSmokeModifiers`) and -`python3 Contracts/SolidityImportSmoke/Structs/scripts/structs_test.py` -(after `lake build SolidityImportSmokeStructs`). The structs suite also -imports a named constructor that lists `{who, amount}` out of member order -(`Expr.pairRev` evaluates source order then packs `uint256 × address`). - -## Current Audit State - -- Lean proof placeholders: 0 `sorry` in compiler/proof modules. -- Project-level Lean axioms: 0. See `AXIOMS.md`. -- Authoritative safe-body Yul runtime target: pinned `lfglabs-dev/EVMYulLean`. -- The previous reference-comparison modules have been removed from the live - proof tree; native EVMYulLean is the checked runtime boundary. -- Yul-to-bytecode compilation remains trusted through pinned `solc` 0.8.33. -- Consumer-declared intrinsics are consumer-owned trust boundaries. The CLZ - prototype keeps Verity at 0 project-level axioms and requires downstream - packages to document any assumed intrinsic obligation they declare. -- Gas safety is not modeled by the semantic preservation theorems. - -## Issue #1722: EVMYulLean Semantic Target - -Status: full semantic integration for safe compiler-produced bodies. - -### Event Emission Proof-Model Alignment (2026-06) - -Source-semantics `normalizeEventValue` (Compiler/Proofs/IRGeneration/SourceSemantics.lean) -now includes a `.newtypeOf _ baseType => normalizeEventValue baseType value` arm, mirroring -the compiler's newtype erasure in `normalizeEventWord` (Compiler/CompilationModel/EventAbiHelpers.lean). -This proof-model alignment ensures that source event values normalize consistently with -compiled event word normalization for scalar newtype parameters. Affects the event proof -path only (non-events statements are unchanged). - -New proof module `Compiler/Proofs/IRGeneration/GenericInduction/EventBridge.lean` bridges -scalar event emission semantics, discharging the `EventHeadStepSemanticBridgeCatalog.bridge` -obligation with per-statement step lemmas, memory wrapping facts, and normalized-word -equivalence theorems. Proof-only scaffolding; no compiler output changes. - -The current generic scalar-event slice is intentionally top-level only: supported -`emit` statements may appear as function-body heads with scalar parameters and at -most three indexed parameters. The contract-level scalar-events wrapper now lifts -the function-level proof over dispatch when callers supply the scalar-event -list-interface witnesses, and `ContractFeatureTest.lean` includes a top-level -emit smoke contract. Nested emits inside structural statements remain future -work, and the Yul/EndToEnd proof layers still exclude event/log semantics. - -### Compiler-Side Emit-Argument Scope Collection (2026-06) - -`collectStmtNames` (Compiler/CompilationModel/ValidationHelpers.lean) emit arm now returns -`collectExprListNames args` rather than `eventName :: collectExprListNames args`. Event -argument expressions now participate in scope-name collection for statement sequence -validation; the event name itself is resolved against the event table and does not enter -the identifier scope. Enables scope-aware event argument validation without shadowing -event name resolution. - -### Proof-Side Scratch Memory Wrapping (2026-06) - -Memory-access functions in the proof IR model now wrap byte/word offsets modulo `2^256` -to match compiled code's wrapping `add` builtin semantics. Affected functions: -- `memorySliceWords` (IRInterpreter.lean): word-reading offset wrapping -- `yulLogDataWords` (IRInterpreter.lean): log data extraction offset wrapping -- `writeEventSignatureScratchFrom`, `writeUnindexedEventScratchFrom` (SourceSemantics.lean): - scratch memory write offset wrapping - -Ensures proof-side scratch addressing matches compiled emit block's offset arithmetic -under EVM `Uint256` wrapping, preventing false misalignment in the semantic bridge. - -The EVMYulLean transition moved the safe-body EndToEnd runtime target from -Verity-owned Yul builtin scaffolding to native EVMYulLean runtime execution. -The current proof surface has: - -- 36 of 36 builtin bridge theorems proven. -- 0 admitted bridge lemmas. -- `smod` and `sar` bridge equivalences fully discharged. -- `compileStmtList_always_bridged` proven for `BridgedSafeStmts`. -- Public native EndToEnd wrappers whose runtime target is - `EvmYul.Yul.callDispatcher`. - -The external-call/function-table family -(`internalCall`, `internalCallAssign`, `externalCallBind`, and `ecm`) now has -function-table-aware closure scaffolding in -`Compiler/Proofs/YulGeneration/Backends/EvmYulLeanCallClosure.lean`. The -public surface — `BridgedSourceInternalCallStmt`, -`BridgedSourceExternalCallBindStmt`, `BridgedSourceEcmStmt` (with the -per-module `ECMBridgeable` obligation), and the corresponding -`compileStmt_*_bridged` / `compileStmtList_*_bridged` closure theorems — -discharges `BridgedStmts` against a `BridgedFunctionTable`. The composition -lemma `BridgedStmts_of_compileStmtList_append` lets concrete contracts -chain these closures with `compileStmtList_always_bridged` across an -arbitrary `pfx ++ sfx` split. End-to-end smoke proof in the same file. -Wiring of `SupportedFragment` / `SupportedSpec` for contracts that -actually use this family is the next milestone. - -## Reentrancy Rely-Guarantee Framework (2026-06) - -A new proof-level reentrancy framework, complementary to the runtime -`nonreentrant` transient-storage guard (#1893). Additive and axiom-free; it -keeps the "Project-level Lean axioms: 0" invariant above. - -- **New modules** (no `sorry`, no `axiom`, no `native_decide`): - - `Verity/Core/Invariant.lean` — state-polymorphic rely-guarantee library - (`Preserves` / `runSeq` / `runSeq_preserves`). - - `Verity/Core/Reentrancy.lean` — grounds it against the `Contract` monad: - `reentrantCall`, `ContractPreserves`, `ReentrancySpec` and the - whole-interleaving meta-theorem `ReentrancySpec.schedule_preserves`, plus a - forward-compatible multi-contract layer (`System` / `lift` / `Isys` / - `cross_contract_schedule_preserves`). -- **New semantic surface**: `Env.reenter : ContractState → ContractState` - (default `id`, the no-reentry case) on `Verity/Core/Semantics.lean`. -- **Worked example**: `Contracts/ReentrancyRelyGuarantee` machine-checks the - Midnight `take`/`liquidate` callback bug — the no-lock `take` admits a - permanent bad-debt liquidation; the locked `take` admits none for any - lock-respecting adversary. -- **Trust boundary**: author obligations (entrypoint-registry completeness, - adversary-model fidelity, invariant adequacy). See the - "Reentrancy Rely-Guarantee Framework" section in `TRUST_ASSUMPTIONS.md`. -- **CI state**: the example is registered in `test/property_manifest.json` and - classified proof-only in `test/property_exclusions.json` (abstract - state-transformer proofs, no compiled bytecode to property- or - differential-test); structure/Foundry-test exemptions are recorded in - `scripts/check_contract_structure.py`. `artifacts/verification_status.json` - and `docs/VERIFICATION_STATUS.md` now record 15 example contracts and +8 - proven theorems (all proof-only, so runtime coverage moves 90% → 88% by - construction while proven count rises 283 → 291). - -### Cross-Function Reentrancy Gate (fail-closed) - -A compilation-level gate that complements the proof framework above: -`validateReentrancyDisposition` -([Compiler/CompilationModel/Validation.lean](Compiler/CompilationModel/Validation.lean)), -a dedicated pass run after call well-formedness validation (and independent of -the single-function CEI check in `validateFunctionSpec`), -now **rejects** any non-`view`/`pure` function whose body makes a direct -external call unless it carries a sound reentrancy disposition. This closes the -cross-function reentrancy class (Midnight `take`/`liquidate`) at the toolchain -boundary: a CEI-clean function is no longer enough, because its external -callback still leaves a transiently-exploitable state live for a reentrant -sibling entrypoint. - -- **Accept-set** (sound only): `nonreentrant()` (runtime transient-storage - guard, #1893) or `reentrancy_trusted` (new metadata-only, unproven author - assertion — emits no code, no proof obligation, recorded as a trust boundary). -- **Deliberately rejected**: `cei_safe` and `allow_post_interaction_writes` - concern single-function CEI ordering only and do **not** satisfy the gate. -- **Internal-helper closure** (Bugbot HIGH on #2032): the `nonreentrant()` - transient guard is attached **only** at the external dispatch boundary - (`attachNonReentrantGuard`, #1893), so it is absent on the lock-free - internal-helper shadow the macro emits for direct intra-contract calls. A - *lock-only* `nonreentrant` function (no `reentrancy_trusted`) is therefore - **rejected when invoked as an internal helper** at macro lowering - (`ensureCallableAsInternalHelper`, - [Verity/Macro/Internal.lean](Verity/Macro/Internal.lean)); routing such a call - through the shadow would run the guarded body without the lock and silently - bypass its only protection. A callee that *also* carries `reentrancy_trusted` - is accepted (the assertion covers the lock-free internal path). This mirrors - the existing `internal nonreentrant()` rejection - (`NonreentrantInternalHelperRejected`, #1971). -- **CEISafety demoted**: `Compiler.Proofs.IRGeneration.CEISafety` - (`CEIProofBackedExecution`) remains a valid proof surface, but it certifies - *single-function* Checks-Effects-Interactions ordering — it is **not** a - reentrancy defense and is no longer presented as one. Cross-function - reentrancy safety is owned solely by this gate's accept-set - (`nonreentrant`/`reentrancy_trusted`); CEI is subordinate ordering hygiene. -- **New annotation surface**: `reentrancy_trusted` threads through the macro - (`Verity/Macro/Syntax.lean` → `Translate/Parsing.lean` → `Types.lean` → - `Translate.lean`) onto `FunctionDecl.reentrancyTrusted` / - `FunctionSpec.reentrancyTrusted` (defaulted `false`, backward compatible). -- **Regression evidence**: `Contracts/Smoke/SecurityCombos.lean` pairs - `ReentrancyDispositionRequired` (a CEI-clean `take` rejected by the gate, - pinned with `#guard_msgs`) against `ReentrancyDispositionDeclared` (the - identical body accepted once it declares `reentrancy_trusted`) — identical - bodies, opposite verdicts, the Midnight shape proven unable to pass - `#check_contract` undeclared. -- **Axiom-free**: the gate is a pure validation predicate; `AXIOMS.md` is - unchanged. Trust-boundary prose is in the "Cross-Function Reentrancy Gate" - section of `TRUST_ASSUMPTIONS.md`. - -## External-Call Journal (2026-08) - -- `ContractState.calls` (`Verity.ExternalCall` entries) is a new append-only, - defaulted field: a proof-side observable of external-call boundaries, not - EVM state. No semantic-preservation claim covers it. -- `DenoteExternalCalls.denoteCall` is unchanged; the journal is added by the - definitionally layered `denoteCallJournaled` / `denoteJournaled`, so all - previously proven world/gas/rollback laws hold verbatim. -- Source-level observation is `DenoteExternalCalls.externalCall : - AdversaryModel → CallSite → Contract ExternalCallResult` - (`Verity/Core/Model/ContractExternalCall.lean`), which reports - callee failure/revert in-band and never raises a monadic revert; - `externalCallRequireSuccess` opts into bubbling snapshot rollback. -- Semantic choices (journal survives caller-side rollback; adversary cannot - write the journal) are documented in `TRUST_ASSUMPTIONS.md` §"External-Call - Journal". -- **Axiom-free**: `AXIOMS.md` unchanged. - -## FunctionSpec Calls, ETH Value, Multi-Contract World (2026-08) - -- `Denote.evalExpr` / `execStmt` still map `Expr.call` and - `Stmt.externalCallBind` to `none` / `.revert`, so - `DenoteAgreement` stays definitional against `SourceSemantics`. - The widened fragment is `Verity.Core.Model.DenoteFunctionCalls`: - `evalExprCall` / `execExternalCallBind` take a `CallEnv` - (`AdversaryModel` + link-time target / value / siteId), debit - `selfBalance` only on a successful `call`, and journal the real - target and value. -- Base `withTransactionContext` is unchanged (keeps - `DenoteAgreement` / `_frame_holds` stable). Payable calls use - `withPayableCallContext`, which credits `selfBalance` with - `msg.value`. `MultiContract.withCallContext` does the same on - each hop. -- EDSL `Contracts.externalCallBind` stays name-keyed (`target`/`value` - = 0). `externalCallBindTo` records target and value and debits ETH - on success. Callee state is not in that single-world stub. -- `Verity.MultiContract` is a finite `Address → ContractState` world - with ETH-valued hops. The P-ETH-1 ensemble is - Bus → Gateway → Vault → (Lido | request). Model-plane only: those - names are not compiled contracts and not an L2 claim. -- Zero new axioms. - -## Execution-Backed External Call Frames (2026-08) - -- `MultiContract.CallFrame` retains distinct caller-before, callee-before, and - callee-entry states. `callEntry` checks distinct accounts, `call` kind, - target/address agreement, and sufficient caller balance before producing a - frame. -- `MultiContract.executeCall` consumes a `CalleeExecution`; its produced - control and returndata determine commit/rollback and the journal entry. - Success commits the caller debit and callee post-state. Failure/revert - preserve both snapshots (apart from the caller's append-only observation). -- `DenoteFunctionCalls.executeFunctionWithCalls` retains the FunctionSpec - post-world and execution control. `runFunctionInFrame` is the first - source-shaped adapter into the framed boundary; `callFunction` is the - official checked composition and does not accept a separately supplied - observation or post-state. The legacy `DenoteResult` projection is derived - from the same execution. -- This slice deliberately supports ordinary `call` only. `staticcall` and - `delegatecall` need distinct frame invariants before admission. -- Zero new axioms. - -## EDSL Executable Plane: Linked External Calls Journal (2026-08) - -- The EDSL executable stubs for linked external calls are no longer silent: - `Contracts.externalCallBind`, `Contracts.callResultWords`, - `Contracts.tryExternalCallWords`, and the `safeTransfer`/`safeApprove` - family now append a `Verity.ExternalCall` entry to `ContractState.calls` - (via `Contracts.linkedCallEntry`/`recordLinkedCall`). Previously they were - `pure ()`-shaped no-ops, so any executable-plane theorem "about" an - external call was vacuously insensitive to duplicated, omitted, reordered, - renamed, or argument-mutated calls. -- General linked externals are name-keyed (address bound at link time), so - `ExternalCall` gained a defaulted `name : String := ""` field; model-plane - entries (`DenoteExternalCalls.journalEntry`) leave it `""` and are otherwise - unchanged. Their executable-plane `siteId`/`target` are `0`. ERC-20 write - wrappers instead record the token address as `target` and the actual wrapper - arguments (excluding that target) as `calldata`; all retain the wrapper name. - Journal position records call order. -- `ExternalArg.toWords` is a canonical, content-preserving journal encoding: - scalars occupy one word, while arrays and byte arrays carry a length prefix - followed by every recursively encoded element/byte. It deliberately does not - claim byte-for-byte EVM ABI layout, but unlike the former size-only word it - distinguishes same-length content mutations. -- In-band results remain deterministic stubs (`externalCallStubWord`); - `callResultWords` and successful `tryExternalCallWords` with a supported - single-word result decode and return the same word recorded in journal - returndata. Aggregate/no-result executable stubs retain their inhabited - default, as do failures. Both report - `success := (name != "fail")` (`externalCallStubSuccess`), giving specs a - reserved name to exercise failure paths. `externalCallWords` (the pure - expression form) still returns the stub word without journaling — it is - not monadic and cannot observe state; this remaining gap is documented in - `TRUST_ASSUMPTIONS.md`. -- The model plane (`DenoteExternalCalls`, `ContractExternalCall`, - `ExternalCallResult.control`) is untouched; all its laws hold verbatim. -- Discriminating evidence: `Contracts/Smoke/ExternalCallObservability.lean` - separates duplicated/omitted/reordered/renamed/zeroed-arg and equal-length - dynamic-content mutants over arbitrary pre-states, pins returned-value / - returndata agreement and ERC-20 targets/calldata, and pins the - revert-rolls-back-journal behaviour of `Contract.run`. -- **Axiom-free**: `AXIOMS.md` unchanged. - -## Guarded Event Preservation + Checked Arithmetic Completion (2026-08) - -- `Compiler/Proofs/IRGeneration/GuardedScalarEvents.lean`: - `compile_preserves_semantics_guarded_with_scalar_events` closes final-result - event preservation (`encodeEvents source.events = ir.events`) for the - guarded whole-contract pipeline on the scalar-event fragment, by proving - the guarded source semantics collapses to the plain one when every - dispatched function is lock-free (which `SupportedSpecWithScalarEvents` - forces). Pure composition of the #2000 event lane and the #2314–#2317 - guarded family; no new semantic assumption, no axiom. -- The `notModeledEventEmission` trust slice and `--deny-event-emission` gate - are **kept**: they flag `rawLog`, which remains unmodeled. Known limit - (documented in `TRUST_ASSUMPTIONS.md` §Event Emission): the slice does not - enumerate declared `Stmt.emit` sites outside the scalar fragment; that - boundary is enforced at the theorem support witness instead. -- `Verity/Core/Uint256.lean`: `checkedSub`/`checkedMul` + `subNoWrap`/`mulNoWrap` - complete the checked-arithmetic lane (#1993) alongside `checkedAdd`. -- Solidity-0.8-style unsigned arithmetic binds now lower to structured panic control - flow. `Verity/Macro/Translate/Expr.lean` emits an exact failure predicate, - `Stmt.panic .arithmeticOverflow` for overflow/underflow or - `Stmt.panic .divisionByZero` for division by zero, and then the arithmetic - result binding. Direct Lean helpers retain compatibility diagnostic strings; - macro-generated compilation bypasses them and emits canonical - `Panic(uint256)` ABI data rather than `Error(string)`. -- `Verity.Core.PanicCode` is closed over those two arithmetic codes. General - runtime `panic(code)` and generated enum guards such as `panic(0x21)` retain - the raw `Stmt.panicCode Expr` compatibility path. -- Helper discovery and the post-codegen Yul rewrite are fail-closed over the - generated shape: `Compiler/CompilationModel/UsageAnalysis.lean` requires an - exact pair of adjacent statements—a guarded typed panic followed by the - arithmetic result binding—which together form the guard/panic/arithmetic - sequence. `Compiler/CodegenCommon.lean` requires the typed direct-literal - compiled panic body and corresponding guard before replacing that pair with - a checked helper call. All four rewrites additionally require both operands - to be numeric literals or variable references; calls and compound operands - remain unchanged. `Stmt.unsafeYul` lowering surrounds raw fragments - with comment-only provenance markers; the same optimizer copies marked - regions verbatim and never matches across or descends into them, so - side-effecting handwritten operands cannot be evaluated a different number - of times by this peephole. - ECM uses distinct boundaries and is inspected automatically under the same - literal/variable operand checks, without author opt-in. Deployment and runtime - each require the actual six canonical helper definitions (four arithmetic, - two panic), exactly once at top level, with no conflicting bindings anywhere - in that section. ECM alone does not insert helpers. Region boundaries prevent - cross-region matches; malformed markers disable the pass. Reserved ECM marker - text in module output makes that module opaque. The ECM bridge wrapper proof - preserves syntactic support only, not optimizer semantic correctness. - Regression cases reject standalone panics, wrong panic codes, reversed - guards, mismatched operands, and raw lookalikes; generated-Yul checks cover - all four helper calls, selector `0x4e487b71`, codes `0x11`/`0x12`, and - `revert(0, 36)`. - The `compiler-regressions` CI job explicitly builds - `Compiler.PanicCodeRegressionTest`; the workflow sync specification requires - that step so it cannot be silently removed. -- `Compiler.Proofs.IRGeneration.execIRStmts_solidityPanicPayload` in `PanicPayloadIR` - proves the abstract IR-interpreter memory/revert result for every in-range - panic code, with typed specializations for both arithmetic constructors. - This result does not retain the revert offset, length, or bytes. - `Compiler.Proofs.YulGeneration.observePanicPayloadBytes_solidityPanicPayload` - in `PanicPayloadBytes` separately proves that the emitted AST returns exactly - the four selector bytes and the 32-byte code word, independently of initial - memory. Its size theorem proves 36 returned bytes. The byte observer uses - EVMYulLean's `mstore` and `evmRevert`; regressions distinguish empty, - truncated, and wrong-offset reverts. This local instruction-sequence proof - does not establish full native Yul execution or solc bytecode correctness. - The generic whole-contract `SupportedSpec` still excludes typed and raw panic - statements through the typed-revert effect surface, and the - `CodegenCommon` peephole rewrite is guarded by structural matching and tests - rather than an end-to-end preservation theorem. Those remaining boundaries - are recorded in `TRUST_ASSUMPTIONS.md` and `docs/ARITHMETIC_PROFILE.md`. -- **Axiom set unchanged**: this slice adds no `axiom` or `sorry` and leaves - `solidityMappingSlot_injective` as the single active project-level axiom. - The net increase in `native_decide` feature tests is tracked in - `artifacts/trust_surface_report.json`; `native_decide` is a documented - trusted reduction mechanism, not a project-level arithmetic axiom. - -## Returndata Surface (2026-08) - -> **Superseded in part by *First-Class Returndata Buffer (2026-08)* below.** The -> constant-zero model recorded here was sound only while the admitted fragment -> issued no call-family instruction. PRs #2398/#2399 added external-call oracle -> lanes, so `returndatasize()` is now read from a real buffer. The -> `returndataCopy` posture and the surface-gating notes below still hold. - -- `Expr.returndataSize` and `Stmt.returndataCopy` are now executed by the - source interpreters, the compiler-free denotation, and the IR interpreter - instead of falling through to an unmodeled revert. The model rests on - EIP-211: a frame's returndata buffer starts empty and is only refilled by a - call-family instruction, and the admitted fragment issues none. So - `evalExpr .returndataSize = some 0`, and `returndataCopy dst src sz` - continues only when `src + sz = 0` and otherwise reverts, matching the EVM's - exceptional halt on `src + size > returndatasize`. -- The generic proof fragment admits only the zero-extent shape, - `SupportedStmtList.returndataCopyEmptySingle` for - `Stmt.returndataCopy dst (.literal 0) (.literal 0)`. Because that copy writes - nothing, it needs no linear-memory model and preserves - `runtimeStateMatchesIR` definitionally. -- `Expr.returndataSize` is now part of `ExprCompileCore`: its compilation to - `returndatasize()` is proved against the same no-call invariant, so it can - occur in generic-fragment expressions without admitting a call-family - instruction or non-empty returndata. -- Admitting that constructor required dropping `.returndataSize` from - `exprTouchesUnsupportedCoreSurface` and `exprTouchesUnsupportedContractSurface`, - the last two gates that still rejected it. The call, state, helper, foreign, - and low-level gates already returned `false` for it. The call-family - constructors (`.call`, `.staticcall`, `.delegatecall`, `.externalCall`, - `.internalCall`) stay gated, which is what keeps the no-call invariant that - makes `returndatasize()` zero. -- `stmtTouchesUnsupported*Surface` previously returned `false` for - `.returndataCopy` without recursing into its subexpressions. That was sound - only while the statement always reverted; the ten recursive predicates now - mirror `calldatacopy` and recurse. `StmtListScopeCore` / - `StmtListScopeDiscipline` gained matching constructors, since the case is no - longer vacuous. -- No new axiom and no new trust assumption: this narrows an unsupported - surface rather than assuming one. `Expr.returndataOptionalBoolAt`, - `Stmt.revertReturndata`, `extcodesize`, and `externalCallBind` remain - unmodeled. - -## First-Class Returndata Buffer (2026-08) - -- Supersedes the "no-call invariant" justification recorded under *Returndata - Surface* above. That section rested on the admitted fragment issuing no - call-family instruction, so a frame's EIP-211 buffer stayed empty and - `returndatasize()` could be modeled as the constant `0`. Issue #2084 - (PRs #2398, #2399) added the external-call oracle lanes - `Stmt.externalCallBind`, `Stmt.tryExternalCallBind` and `Stmt.ecm` to the - source semantics and the compiler-free denotation, which invalidated that - premise: a modeled call can now return words, and the constant `0` would have - been an unsound read of the resulting frame. -- `Verity.ContractState` and `IRState` each gained a `returndata : List Nat` - field holding the callee's returned words. All three oracle lanes install the - receipt's `returnValues` through `SourceSemantics.returndataAfterCall`, on the - failure path as well as the success path, matching EIP-211: the buffer is - replaced by every call-family instruction regardless of outcome. -- `ContractState.returndataSize` is `32 * returndata.length` normalized to a - word, and `ContractState.returndataOptionalBool` reproduces the - `or(eq(returndatasize(), 0), and(eq(returndatasize(), 32), eq(mload(out), 1)))` - shape the compiler emits for ERC-20 style callees. `Expr.returndataSize` and - `Expr.returndataOptionalBoolAt` now denote those functions at all three planes - (`SourceSemantics.evalExpr` / `evalExprWithHelpers`, `Denote.evalExpr`, and - `evalIRExpr` via the new `returndatasize` builtin branch), and the value-bound - induction discharges both from `Uint256.isLt` rather than from a constant. -- `runtimeStateMatchesIR` and `constructorRuntimeStateMatchesIR` gained an - eighteenth conjunct, `state.returndata = runtime.world.returndata`, exposed as - `runtimeStateMatchesIR_returndata`. `initialIRStateForTx` seeds it from the - initial world. `evalIRExpr_returndataSize_of_runtimeStateMatchesIR` and - `eval_compileExpr_returndataOptionalBoolAt_of_compiled` are reproved against - the real buffer instead of against `some 0`. -- `execStmt_ecm_static_preserves_world_modulo_memory_calls_and_returndata` is - the renamed static-ECM preservation lemma; the buffer now joins memory and the - call log in the modulo set, since a static call still refills it. -- No unsupported-surface predicate changed. `stmtTouchesUnsupported*Surface` - keeps gating the call-family constructors exactly as before, so no headline - theorem gains coverage and behaviour is identical at the default empty buffer. - This is a soundness repair of the model, not a fragment widening. -- `Stmt.returndataCopy` stays conservative on both layers: `src + size = 0` - continues (it writes nothing whatever the buffer holds) and every other extent - is the EVM's exceptional halt, `.revert`. `SourceSemantics` and - `IRInterpreter` branch identically, so the two layers still agree on the nose. - Only the justifying comments needed correcting — "the buffer is empty in this - fragment" is now false, while "we conservatively under-approximate" is true. - **Superseded by *Bounded Returndatacopy (2026-09)* below: in-bounds extents - now perform the copy.** -- No new axiom and no new trust assumption. `Contracts.returndataSize` in the - executable EDSL surface remains the constant-zero stub, alongside its - `calldatasize` / `mload` / `extcodesize` placeholder siblings. - -## EDSL Executable Plane: Hashed Nested/Struct Mappings (2026-09) - -- `Contracts.getMappingN` / `setMappingN` / `getMappingWord` / - `setMappingWord` were `pure 0` / `pure ()` stubs: nested and struct - mappings had no executable state, so executable-plane theorems about - them were vacuous (#2416, Pareto G2). They now read and write the `.slot` - channel of `ContractState` at the Solidity keccak slot - (`Compiler.Proofs.abstractMappingSlot`, folded left over the key path; - struct/word offsets added modulo `2^256`), the same derivation the model - plane (`DenoteOracle.mappingSlot`) and the generated struct-mapping - accessors use. `transient` chains use the `.transient` channel. -- `structMembers` / `structMembers2` destructuring lowers to per-member - `structMember` reads in the executable plane. -- Hashed slots live in the `.slot` channel, disjoint from the - constructor-keyed `map` / `mapUint` / `map2` channels; distinctness of - slots under different keys uses the existing `solidityMappingSlot_injective` - axiom, adjacent struct words are distinct without it. -- Smoke: `Contracts/Smoke/HashedMappings.lean`. Zero new axioms. - -## EDSL Executable Plane: Getter Hops and Deferred Links (2026-09) - -- G23 (#2439): bound typed calls whose method names a public storage field of - the callee lower to `Contract.hopCallView target ()` - instead of applying the `StorageSlot` constant as a function (which failed - to elaborate). `Bool` getters decode the 0/1 word as `word != 0`. Packed, - transient and non-Uint256/Address field shapes fail closed at elaboration. -- G15 (#2415): `linked_contracts name : IFace := deferred` for cyclic - bindings. Typed calls on a deferred binding keep the ABI external-call - lowering but always use the threaded `ExecutableCallContext`; the context - binder is propagated through the existing helper fixed point. Fidelity is - by instantiation: `AdversaryModel.withViewLinks` answers selected static - sites by a linked body in a view hop, with the fidelity lemma - `Contracts.externalStaticCallContractWordsTo_withViewLinks` (and a revert - counterpart). With the stub context results are unchanged. -- Compilation-model lowering is unchanged for both. Smoke: - `Contracts/Smoke/LinkedGettersAndDeferred.lean`. Zero new axioms. -- G26: a bound typed call whose resolved callee constant takes the - `ExecutableCallContext` forwarded the caller's context, but a caller with no - reentrancy window of its own had the fixed `AdversaryModel.stub` as its - context, so the callee's nested deferred calls were answered by the stub. - Such a bound call now makes the enclosing function take the context - (`bodyNeedsLinkedCallContext`, same helper fixed point as G15). Smoke: - `Contracts/Smoke/HopContextAndMutableTuples.lean` (`wrap_stub` vs - `wrap_withViewLinks`, `viaHelper_withViewLinks`). -- G14 residual: typed interface tuple calls are no longer restricted to `view` - methods. Mutable tuple calls lower to `Contract.hopCall` (bound), the - mutable ABI external call with arity = number of results (unbound / - deferred), and `Compiler.Modules.Calls.withReturnsModule` in the - compilation model (new ECM `externalCallWithReturns`, classified - `abiBoundary`, assumption `external_call_abi_interface`). Smoke: - `run_bound_executes_callee`, `run_bound_revert_bubbles`, - `run_unbound_stub`, `run_deferred_stub`. -- G25 (documented rule, no code): named bindings dispatch by interface, not by - runtime target; an interface used on several different contracts must be - `deferred` and answered by `withViewLinks` keyed by target. - -## Bounded Returndatacopy (2026-09) - -- Supersedes the conservative `returndataCopy` posture recorded above. Once the - EIP-211 buffer is first-class (#2400), a `returndataCopy dst src size` whose - extent fits (`src + size ≤ 32 * buffer.length`) copies the buffer's bytes - into memory instead of reverting — complete words are replaced and, when - `size % 32 ≠ 0`, the final partial destination word is merged with its - untouched low bytes; only out-of-bounds extents - are observed as the EVM's exceptional halt, `.revert`. The semantics is - identical on all four lanes — `SourceSemantics.execStmt` and - `execStmtWithHelpers`, the compiler-free `Denote.execStmt`, and the IR - interpreter's `evalIRCall` / `evalIRCallWithInternals` — so the layers still - agree on the nose. -- The word model mirrors the `calldatacopy` lane - (`Compiler.Proofs.YulGeneration.Calldata`): `returndataloadWord` is the - byte-addressed, zero-extended read of the word list (no selector prefix), the - destination region is the shared `calldatacopyWritesAt dst size` word range, - and `returndatacopyMemoryPadded` is the IR-side memory update: the - complete-word kernel `returndatacopyMemory` plus, for `size % 32 ≠ 0`, the - ceiling-word merge that writes the high `size % 32` copied bytes while the - low bytes survive from memory — the returndata analogue of the - calldata lane's `calldatacopyMemoryPadded`. Every copied word is - below the EVM modulus (`returndataloadWord_lt_evmModulus`), so - `Uint256.ofNat` wrapping is the identity on the copied region. -- `runtimeStateMatchesIR_returndatacopyBothMemory` is the new reusable bridge: - when source and IR memories matched before the step, they match after the - copy, using the eighteenth `runtimeStateMatchesIR` conjunct to pin the - buffers equal. `IRStmtPreservesObsAt_of_returndatacopy` is generalized from - the zero-extent shape to every in-bounds extent. -- `compiledStmtStep_returndatacopy_empty_single` is reproved: the zero-extent - copy fits every buffer and still writes nothing, so the admitted fragment - behaves exactly as before (`returndatacopyMemory_zero`). New: - `compiledStmtStep_returndatacopy_bounded_single` proves the compiled single - `returndatacopy(destIR, srcIR, sizeIR)` step corresponds to - `Stmt.returndataCopy destOffset sourceOffset size` for *every* extent — both - layers branch on the same fit guard, so no side condition is needed. -- Partial-extent repair (review round on #2401): the bounded update first - shipped as the complete-word kernel alone, so an in-bounds copy with - `size % 32 ≠ 0` — a one-byte copy being the smallest case — left the final - destination word unchanged, i.e. the copy was modelled as a no-op there. All - four lanes now apply the padded update above, and - `returndataCopy_one_byte_merges_ceiling_word` kernel-checks the regression: - the copied high byte lands and the 31-byte destination suffix survives. - `compiledStmtStep_returndatacopy_bounded_single` stays exposed for every - extent — no word-alignment restriction was added and no previously supported - behaviour was reclassified. -- No unsupported-surface predicate changed: the generic proof fragment still - admits only `returndataCopyEmptySingle`, so no headline theorem gains - coverage. `Contracts.returndataCopy` in the executable EDSL remains a no-op - stub, next to the constant-zero `Contracts.returndataSize`. -- No new axiom and no new trust assumption. - -## Pure `exp` Builtin Lane (2026-08) - -- `pow a b` / `a ^ b` in the EDSL lowers to - `Expr.externalCall builtinExpName [base, exponent]`, a reserved sentinel that - the compiler already lowered to the pure Yul `exp` builtin rather than to a - foreign call. The node shape made it look like an external call to every - proof-side surface predicate, so the whole arm was gated as Tier-4 foreign - behaviour. That gate is now keyed on the sentinel: the eight - `exprTouchesUnsupported*Surface` / `exprTouchesInternalHelperSurface` - predicates recurse into `base`/`exponent` for `builtinExpName` at arity two - and keep their previous fail-closed constant for every other - `Expr.externalCall`. -- The lane is executed, not assumed, at all four planes: `SourceSemantics` - (`evalExpr`, `evalExprWithHelpers`), the compiler-free denotation - (`Denote.evalExpr`, with `DenoteAgreement` extended), the IR interpreter - (which routes `"exp"` to the EVMYulLean backend), and EVMYulLean itself. - All four denote `Uint256.pow`, i.e. `(a % 2^256) ^ (b % 2^256) % 2^256`. -- `evalPureBuiltinViaEvmYulLean_exp_native` closes the backend leg by proving - `EvmYul.UInt256.exp` equals that value, via `uint256_powAux_toNat` / - `uint256_pow_toNat` on the square-and-multiply loop. No `native_decide`. -- `ExprCompileCore.builtinExp` admits the lane to the generic compile core, so - `pow` may appear in generic-fragment expressions, `require` conditions, and - `emit` arguments. `compileExpr_builtinExp_ok` and - `eval_compileExpr_builtinExp_of_compiled` prove compilation and evaluation - agreement against `YulExpr.call "exp"`. -- `collectExprNames` no longer reports `builtinExpName` as a callee identifier. - The sentinel never reaches generated Yul (it becomes `exp`) and is already a - reserved name, so it cannot collide with a compiler-generated temp. This - matches `TrustSurface.collectExternalExprNames`, which has always skipped it, - and it restores `collectExprNames ⊆ exprBoundNames` on the compile core. -- No new axiom and no new trust assumption: this eliminates an unsupported - surface rather than assuming one, and it removes `pow` from the assumed - external-call bucket. Genuine `Expr.externalCall` targets, `Expr.call` / - `staticcall` / `delegatecall`, and `Stmt.externalCallBind` remain gated. - -## Audit Artifacts - -| Artifact | Purpose | Check | -|----------|---------|-------| -| `artifacts/evmyullean_native_lowering_report.json` | Native lowering coverage, admitted bridge lemmas, safe-body integration status | `python3 scripts/generate_evmyullean_native_lowering_report.py --check` | -| `artifacts/evmyullean_fork_audit.json` | Pinned fork divergence and non-semantic fork delta | `python3 scripts/generate_evmyullean_fork_audit.py --check` | -| `artifacts/evmyullean_capability_report.json` | EVMYulLean capability surface and reference-oracle paths | `python3 scripts/generate_evmyullean_capability_report.py --check` | -| `artifacts/storage_layout_report.json` + `artifacts/STORAGE_LAYOUT_SUMMARY.md` | Per-contract storage layout for migration/audit review: explicit slots, alias ranges, reserved ranges, packed subfields, mappings, dynamic arrays, opt-in namespaces (#1897) | `python3 scripts/generate_storage_layout_report.py --check --no-lean` (drift gate in `make check`); regenerate with `make regen-storage-layout-report` | -| `PrintAxioms.lean` / generated axiom report | Axiom dependency visibility | `python3 scripts/generate_print_axioms.py --check` and `lake build PrintAxioms` | -| `Compiler.Proofs.IRGeneration.IntrinsicProofs` | Proven Verity-owned intrinsic plumbing: scope accounting, generic lowering shape, fork-order facts, and arity rejection | `lake build Compiler.Proofs.IRGeneration.IntrinsicProofs` | -| Intrinsic fork gate | Fail-closed `min_fork` enforcement against `--target-fork` / `YulEmitOptions.targetFork` | `lake build Compiler.CompileDriverTest` | -| `trust_report.intrinsics[*]` | Planned consumer-declared intrinsic trust surface: name, emission mode, opcode/builtin target, obligation, `min_fork`, and source location | Follow-up hardening; until then, grep consumer trees for `verity_intrinsic` | - -## Storage-Lens API Freeze — C5 Step 1 (2026-08) - -- `scripts/check_storage_lens_freeze.py` (in `make check`) ratchets raw - `ContractState` storage-channel record updates: new - `{ s with storageMap := ... }`-style sites fail CI; the 304 existing sites - are frozen per file in the script's `BASELINE` and must only shrink. - Exactly-lens-shaped helpers (`setLock`, `ReentrancyExample.setStorageSlot`/ - `setMappingSlot`, adversary transitions in the call-program examples, - `TypedIRCompilerCorrectness` typed-IR write helpers) are already migrated - to `writeSlot`/`writeMap`/`writeTransient`. -- This is step 1 of the C5 storage-representation flip (single word-addressed - map + Solidity-layout slot derivation under stable lens names); steps 2–4 - (proofs onto `storage_simps`, representation flip, compiler slot - correspondence) are tracked in `docs/ROADMAP.md`. No semantic change: the - lenses are definitionally the former raw updates. - -## Storage-Lens Simp Flip — C5 Step 2 (2026-08) - -- The ContractState lenses are no longer default-`simp` transparent: the - global `attribute [simp]` block in `Verity/Core.lean` is removed. - `storage_simps` stays laws-only (read-over-write normalization; it - deliberately does not unfold lenses to the raw record representation, so it - survives the step-3 flip). Proofs that genuinely need the current raw - representation now name the lens explicitly in their `simp` lists - (`simp [ContractState.writeSlot]`) — those call sites are the step-3 - burn-down inventory, greppable as `ContractState.read`/`ContractState.write` - occurrences inside simp lists. -- Repair surface: ~25 files (Core storage-array run-lemmas, Stdlib - Automation/MappingAutomation, NonReentrantGuard, the `denote_stmt_arm` - macro in `DenoteAgreement`, the `simp_tir_eval` macro in - `TypedIRCompilerCorrectness`, and the contract proof suites). No theorem - statement changed; no semantic change; zero axioms. - -## StorageKey Canonical Backing — C5 Step 3 (2026-08) - -- `ContractState` now stores word-valued channels in one - `storageWords : StorageKey → Uint256` map. `StorageKey` is an injective - inductive (`slot` / `contractSlot` / `transient` / `addr` / `map` / - `mapUint` / `map2`). Solidity keccak slot derivation stays on the - compiler side; lens laws use constructor injectivity, not hash - injectivity. -- Public accessors keep the old names (`storage`, `storageAddr`, - `storageMap`, …). Specs that read those views do not change shape. - `storageArray` and `knownAddresses` remain separate fields this step; - independence from the word-channel lenses is now proved - (`Compiler.Proofs.Storage.SeparateChannels`). -- Four-feature increment (not a C5 fold): `FieldType.mappingFixedArray` - compiles element reads/writes at `mappingSlot(slot, key) + i`; - CodeData/SSTORE2 layout theorems pin the `STOP` prefix and offset-1 - read; transient `fixedArrayUint128` uses `tload`/`tstore` and - `fieldRootKey` maps it to `StorageKey.transient` (`storageKeySlot = - none`); `denoteSelfDelegateCalls` is a first-class self-delegate - sequence (not the ECM, not #2365's compiled `FunctionSpec` path). - Zero new axioms. -- The freeze baseline shrinks to `Verity/Core.lean` (lens - implementations) plus `Contracts/TypedIRTests.lean` (IRState field-name - collisions). `storageWords :=` is forbidden outside `Verity/Core.lean`. -- C5 step 4 complete under `solidityMappingSlot_injective` - (not axiom-free): `Compiler.Proofs.Storage.MappingCoherence` - now defines `storageKeySlot` and address-keyed `MappingCoherent`, - with `defaultState_mappingCoherent` / `MappingCoherentUint` / - `MappingCoherentMap2` and the aligned `writeMap`/`writeMapUint`/ - `writeMap2`+`writeSlot` laws (same pair, plus other pairs under an - explicit non-alias hypothesis). `FieldStorageKey` collapses a - CompilationModel field list to a root `StorageKey` (and typed - `map` / `mapUint` / `map2` entries) and proves `storageKeySlot` - of that key is the resolved slot. Finite-set address - `MappingCoherentOn` is preserved by an aligned write under an - explicit pairwise derived-slot certificate - (`MappingCoherenceOn`), including uint and nested-address lists. - Address-keyed `mappingStruct` / `mappingStruct2` members collapse - to `mappingSlotLocation` / `nestedMappingSlotLocation`. Compatibility - `aliasSlots` are extra compiler write targets; the resolved head is - the field's `storageKeySlot`. Cross-channel finite-set preservation - (aligned write on one mapping channel vs another channel's list) - takes an explicit derived-slot inequality. Packed subfields extract - from the `storageKeySlot` word (`packedExtract`); compiler packed-read - composition with SolidityStorage is `fieldPackedExtract_eq_compiledPackedRead` - (`width < 256`). `FieldCoherence` identifies a named mapping field's - `storageKeySlot` with the flat slot `MappingCoherent*` / - `MappingCoherentOn*` reads. - A lone `writeSlot` preserves a finite `*On` list under an explicit - listed-vs-written slot inequality. A lone `writeTransient` preserves - the same lists by constructor injectivity (`StorageKey.transient` - vs persistent `.slot` / `.map` / `.mapUint` / `.map2`); no slot - inequality. Global aligned `writeMap` / `writeMapUint` / - `writeMap2`+`writeSlot` preserves `MappingCoherent*`. - `writeTransient` preserves the globals by constructor injectivity. - A lone `writeSlot` preserves a global under an explicit - image-avoidance `∀` (derived slot ≠ written word). That `∀` is - discharged for writes that never touch `StorageKey.slot`: - `writeTransient` (constructor injectivity), `writeAddrSlot` - (constructor injectivity of `.addr`), and `writeArray` (separate - `storageArray` field). Finite `*On` lists lift from the globals - without a pairwise certificate. Arbitrary `n` is still not claimed - safe for a lone `writeSlot`. - `FieldEncode` derives `findResolvedFieldAtSlot` from - `findFieldWithResolvedSlot` plus no write-slot conflict, persistent, - and unpacked, then identifies that slot with `encodeStorageAt`. - An unoccupied mapping-derived slot (address, uint, or map2) encodes - as the `MappingCoherent*` / `MappingCoherentOn*` shadow. bytes32-keyed - maps collapse to `solidityMappingSlot` of the 32-byte word (no - `StorageKey.mapBytes32`). All nine `MappingType.nested` key-type - pairs collapse to `abstractNestedMappingSlot`. -- C5 step 4 final slice (`Compiler.Proofs.Storage.MappingCoherentGlobal`, - #2330): `storageKeySlot : List Field → StorageKey → Option (Channel × Nat)` - is the field-list collapse — `Channel` is `persistent` / `address` / - `transient` / `contract c`, and `contractSlot 0` is **not** identified - with `slot`. `MappingCoherentAllKeys` is the all-keys (`∀ StorageKey`) - invariant, not a listed pair or a single key. The declared layout is - load-bearing: a mapping key collapses only when the resolved field at its - base slot declares the matching `MappingType`, so distinct mapping shapes - force distinct base slots and `solidityMappingSlot_injective` separates - their derived images. Aligned `writeMap` / `writeMapUint` / `writeMap2` + - `writeSlot`, `writeAddrSlot`, and `writeTransient` preserve it; a lone - `writeSlot` takes `DerivedMappingSlotsAvoid` (the existing image-avoidance - `∀`); the simple-vs-nested cross case takes the layout certificate - `MappingBasesNotDerived`. `readMap_eq_encodeStorageAt_of_coherent` (plus - `mapUint` / `map2` variants) is the lens-read = flat-read bridge under - that invariant. Dynamic-array element slots are an explicit - `unsupported` row, not an equivalence: the source model derives element - `i` at `solidityMappingSlot s i` - (`Compiler/Proofs/IRGeneration/SourceSemantics.lean:330-350`) while the - Yul layout uses `keccak256(bytes32(s)) + i` - (`Compiler/Proofs/Storage/StructArrayStorage.lean:666-671`); the collapse - returns `none` at dynamic-array roots - (`storageKeySlot_slot_dynamicArray`). -- Zero new axioms. - -## Hop Storage Namespacing — G24 (#2440, 2026-09) - -- `StorageKey` gains `scoped (contract : Nat) (key : StorageKey)`: the parked - copy of a non-slot channel key (`addr` / `transient` / `map` / `mapUint` / - `map2`, see `StorageKey.isPlainNonSlot`) of contract `contract`. -- `ContractState.switchSlotWorld parkId loadId` (used by `enterHop` / - `exitHop`, hence `Contract.hopCall` / `Contract.hopCallView`) now namespaces - every word-valued channel: `.slot n` loads `.contractSlot loadId n` (as - before), each plain non-slot key `k` loads `.scoped loadId k`, and the - current plain world is parked under `.contractSlot parkId` / `.scoped parkId`. - Previously address slots, transient slots and mappings were shared between - caller and callee across a hop, which made cross-contract fidelity - statements over address fields or mappings unsound. -- New lemmas (`Verity/Core.lean`): `enterHop_readAddrSlot`, - `enterHop_readTransient`, `enterHop_readMap`, `enterHop_readMapUint`, - `enterHop_readMap2`, `enterHop_storageWords_plainNonSlot`, - `enterHop_scoped_caller`, `exitHop_storageWords_plainNonSlot`, - `exitHop_scoped_callee`, `exitHop_contractSlot_callee`, - `switchSlotWorld_{plainNonSlot,scoped_park,scoped_other,contractSlot_park,contractSlot_other}`, - and the round trip `exitHop_enterHop_storageWords_plain`. - Smoke: `Contracts/Smoke/HopNamespace.lean` (callee address slot / mapping - are its own, caller untouched, second hop sees the parked write, nested - A→B→A reentrant hop sees A's own world, revert restores). -- Compiler storage-coherence collapses map `scoped` keys to `none` - (`MappingCoherence.storageKeySlot`, `MappingCoherentAllKeys.storageKeySlot`): - they have no flat compiler-channel counterpart. -- Remaining boundary: `storageArray` (dynamic arrays, a separate field) is - still global across hops. -- Zero new axioms. - -## CI Guards - -- `make check` validates generated reports, bridge coverage synchronization, - builtin bridge matrix synchronization, Lean hygiene, proof length, and - documentation counters. -- `make test-evmyullean-fork` validates the pinned fork audit, checks the - native lowering report, rebuilds the public EndToEnd EVMYulLean target, and - runs the concrete bridge-equivalence tests. -- `.github/workflows/evmyullean-fork-conformance.yml` runs the EVMYulLean fork - conformance probe weekly. Scheduled or manual failures fail the workflow and - open or update a GitHub issue for drift triage. -- Intrinsic fork enforcement is fail-closed: builds using an intrinsic whose - `min_fork` exceeds the contract target fail unless the caller passes - `--allow-future-fork-intrinsics`. - -## Update Checklist - -1. Update `TRUST_ASSUMPTIONS.md` for the human-readable trust boundary. -2. Update `AXIOMS.md` if axioms, non-axiom trusted primitives, or soundness - controls changed. -3. Update this file with the changed audit state, artifacts, and CI guards. -4. Regenerate deterministic artifacts with the relevant `scripts/generate_*.py` - command. -5. Run `make check`; run targeted Lean builds for changed proof modules. - -**Last Updated**: 2026-06 (cross-function reentrancy gate: fail-closed `validateReentrancyDisposition` rejection of external-call functions lacking a sound disposition, new `reentrancy_trusted` metadata annotation, CEISafety demoted to single-function ordering hygiene, `#guard_msgs` regression pair in `SecurityCombos.lean` — axiom-free; reentrancy rely-guarantee framework: `Verity.Core.Invariant` / `Verity.Core.Reentrancy`, `Env.reenter` hook, Midnight `take`/`liquidate` worked example — additive, 0 axioms; event emission proof-model alignment, emit-argument scope collection, scratch memory wrapping; storage layout audit artifacts, #1897; nonreentrant transient-storage guard, #1893; non-alias certificate write-set overlap, #1967; nonreentrant fork requirement, #1968) diff --git a/CLAUDE.md b/CLAUDE.md index cc37459ff0..508b65774b 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -1,10 +1,8 @@ # Agent Guide for Verity -This file is the operator quickstart. Keep it short and enforceable. - ## Non-Negotiables -1. Keep `AUDIT.md`, `TRUST_ASSUMPTIONS.md`, and `AXIOMS.md` synchronized with any semantic/trust/CI boundary change. +1. Keep `docs/TRUST_ASSUMPTIONS.md` and `docs/AXIOMS.md` synchronized with any semantic, trust or CI boundary change. 2. Never claim completion without evidence and passing checks. ## Core Commands @@ -18,7 +16,7 @@ make test-foundry # Run Foundry differential tests ## Reference Docs -- Project overview: [README.md](README.md) +- Project overview and review order: [README.md](README.md) - Contribution conventions: [CONTRIBUTING.md](CONTRIBUTING.md) - Roadmap: [docs/ROADMAP.md](docs/ROADMAP.md) -- Script reference: [scripts/REFERENCE.md](scripts/REFERENCE.md) +- Solidity import: [docs/SOLIDITY_IMPORT.md](docs/SOLIDITY_IMPORT.md) diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index de714d91ab..78476f19c5 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -1,308 +1,57 @@ # Contributing to Verity -Quick conventions for contributing. See [README.md](README.md) for project overview, [docs/ROADMAP.md](docs/ROADMAP.md) for status. +## Pull requests -## Issue Format +Title with a category prefix (`[Layer 3]`, `[Trust Reduction]`, +`[Compiler Enhancement]`, `[Documentation]`, `[Infrastructure]`, …). The body +has a **Summary**, a **Test Plan** (what you ran), and **Related Issues**. +Issues use the templates in `.github/ISSUE_TEMPLATE/`. -**Title**: `[Category] Brief description` +Before opening a PR: -**Categories**: `[Layer 3]` `[Trust Reduction]` `[Property Coverage]` `[Compiler Enhancement]` `[Documentation]` `[Infrastructure]` - -**Labels**: Use `layer-3` `lean` `proof` `enhancement` `bug` `documentation` `help-wanted` `good-first-issue` - -**Structure**: Use issue templates in `.github/ISSUE_TEMPLATE/` -- Goal (what needs doing) -- Effort estimate (hours/weeks) -- Implementation approach -- Acceptance criteria -- References (file paths, issues) - -## PR Format - -**Title**: Same `[Category]` prefix as issues - -**Body**: -```markdown -## Summary -- Bullet points of changes - -## Test Plan -How you verified it works - -## Related Issues -Closes #X, addresses #Y -``` - -**Before submitting**: -```bash -lake build # Must pass — verifies all proofs -FOUNDRY_PROFILE=difftest forge test # Must pass — runs all Foundry tests -# FOUNDRY_PROFILE=difftest is required because property and differential -# tests use vm.ffi() to invoke the Lean-based interpreter -# No new `sorry` without documentation -# No new `axiom` without documenting in AXIOMS.md -# Update docs/ROADMAP.md if architectural changes -# If adding a new contract, run the checklist below -``` - -**When adding a new contract**, also update: -- Author the contract with `verity_contract`; do not add a separate hand-written `Compiler.Specs` body unless the workflow truly requires a special case like external linking. -- A new reusable facet (Ownable, Pausable, …) is a `verity_mixin` plus named-slot specs, a `Footprint`, and `_meets_spec` / guard theorems. Hosts `include` the mixin; do not copy `Owned.lean` into the host. -- Add the macro-generated `.spec` alias/list entry in [`Contracts/Specs.lean`](Contracts/Specs.lean) if the contract should ship through the default compiler manifest. -- `test/property_manifest.json` — Run `python3 scripts/extract_property_manifest.py` -- `README.md` — Contracts table (theorem count and description) -- `docs/VERIFICATION_STATUS.md` — Contract table and coverage stats -- `docs-site/public/llms.txt` — Quick Facts and theorem breakdown table -- `docs-site/content/verification.mdx` — Snapshot section -- `docs-site/content/examples.mdx` — Contract descriptions and count -- Plus any other files that reference theorem/test/contract counts (e.g., `compiler.mdx`, `research.mdx`, `index.mdx`, `layout.tsx`, `ROADMAP.md`, `TRUST_ASSUMPTIONS.md`, `test/README.md`) -- Run `python3 scripts/check_contract_structure.py` to verify file structure is complete -- Run `python3 scripts/generate_verification_status.py --check` to verify the machine-readable status artifact is current -- Run `python3 scripts/check_lean_hygiene.py` to verify no `#eval` in proof files and `allowUnsafeReducibility` count is correct - -## Adding an Intrinsic - -Declare consumer-owned opcode bindings with `verity_intrinsic` only when the -consumer owns the temporary trust boundary. Keep Verity opcode-agnostic: changes -in this repository should add generic intrinsic mechanics, not opcode-specific -business logic. The consumer repository must document any generated obligation -in its own `AXIOMS.md` or trust-boundary document. Set `min_fork` to the first -fork where the emitted opcode is valid; Verity accepts `cancun`, `prague`, and -`osaka` (`fusaka` is parsed as the Ethereum combined-upgrade alias) and enforces -the declaration against `--target-fork`. - -See [docs/INTRINSICS.md](docs/INTRINSICS.md) for the declaration format, -audit checklist, and worked example. - -## Proof Hygiene Requirements - -Every PR that touches proof files must satisfy all of the following. -Each requirement is enforced by CI and cannot be bypassed without updating -the corresponding enforcement script. - -1. **Zero `sorry`**: `lake build` rejects incomplete proofs at the Lean kernel level. - CI runs an independent grep scan as defense in depth - ([`check_lean_hygiene.py`](scripts/check_lean_hygiene.py)). - -2. **Zero new axioms** without a same-commit update to [AXIOMS.md](AXIOMS.md) - including: justification, risk rating, CI validation strategy, and - elimination path. Enforced by - [`check_axioms.py`](scripts/check_axioms.py). - -3. **Axiom dependency freshness**: If theorems are added or removed, - `PrintAxioms.lean` must be regenerated - (`python3 scripts/generate_print_axioms.py`). - CI validates via `--check` mode. - -4. **Proof size**: Proofs should be under 30 lines. Proofs over 50 lines - require an entry in the allowlist in - [`check_proof_length.py`](scripts/check_proof_length.py) with a - PR comment explaining why decomposition is not feasible. Enforced by CI. - -5. **No `native_decide` in contract proofs**: `native_decide` bypasses the - Lean kernel checker. It is allowed only in smoke tests - (`Compiler/Proofs/YulGeneration/SmokeTests.lean`). Enforced by - [`check_lean_hygiene.py`](scripts/check_lean_hygiene.py). - -6. **No debug commands in proofs**: `#eval`, `#check`, `#print`, `#reduce` are - forbidden in proof files (they slow incremental builds). Enforced by - [`check_lean_hygiene.py`](scripts/check_lean_hygiene.py). - -7. **Verification status freshness**: Regenerate and commit - [`artifacts/verification_status.json`](artifacts/verification_status.json) - when theorem/test/axiom counts change. - Enforced by [`generate_verification_status.py`](scripts/generate_verification_status.py). - -## Code Style - -**Lean**: -- 2-space indentation -- Meaningful names (`irState` not `s`) -- Proofs under 30 lines when possible (see Proof Hygiene above) -- Document complex proof strategies - -**Commits**: -``` -type: description - -[optional body] - -Co-Authored-By: Claude -``` - -Types: `feat` `fix` `proof` `docs` `refactor` `test` `chore` - -## Code Comment Conventions - -Follow these conventions to keep documentation accurate and maintainable. - -### Proof Status - -**For Theorems**: -- `sorry` - Incomplete proof (blocked by CI in proof files) -- No marker - Complete proof -- **DON'T** use TODO/STUB in proven theorems - -```lean --- ✅ GOOD: Complete proof, no marker needed -theorem my_theorem : ... := by - unfold ... - simp [...] - --- ❌ BAD: Complete proof but has misleading TODO -theorem my_theorem : ... := by - -- TODO: This actually works fine - unfold ... - simp [...] -``` - -**For Incomplete Proofs**: -```lean --- Only in draft branches, NOT in main -theorem draft_theorem : ... := by - sorry -- Strategy: Use induction on List structure - -- See issue #123 for details +```sh +lake build # every proof must check +make check # repository checks, as in CI +make test-foundry # property and differential tests (uses vm.ffi) ``` -### Module Documentation - -**Module Headers** should reflect current status: - -```lean -/-! ## Module Name - -Brief description of what this module does. - -**Status**: Complete | Partial | Draft -**Dependencies**: List axioms/external dependencies -**Related**: Links to other relevant modules --/ -``` - -**Status Definitions**: -- **Complete**: All theorems proven, no sorry -- **Partial**: Some proven, some sorry (specify which) -- **Draft**: Structural outline only - -**Example**: -```lean -/-! ## Layer 3: Statement-Level Equivalence - -Proves IR statement execution is equivalent to Yul statement execution -when states are aligned. - -**Status**: Complete - All statement types proven -**Dependencies**: Uses keccak256 axiom (see AXIOMS.md) --/ -``` - -### Future Work Comments - -For planned improvements or known limitations: - -```lean --- FUTURE: Add support for delegatecall --- See issue #123 for design discussion -def currentImplementation := ... -``` - -**DON'T** use: -- ~~`TODO:`~~ (implies incomplete current work) -- ~~`FIXME:`~~ (implies broken code) -- ~~`HACK:`~~ (use `NOTE:` with explanation instead) - -### Implementation Notes - -For non-obvious design choices: - -```lean --- NOTE: We use fuel-based recursion here because Lean cannot prove --- termination for mutually recursive functions with this structure. --- See Equivalence.lean for the proof strategy this enables. -def execIRStmtFuel (fuel : Nat) : ... := ... -``` - -### Axiom Documentation - -All axioms **must** be documented inline **and** in AXIOMS.md: - -```lean -/-- AXIOM: explain the remaining trust boundary - -Document why the declaration cannot yet be proved in Lean, -what guards or regression checks keep it honest, -and link to AXIOMS.md for the current trust story. -See AXIOMS.md for full soundness justification. --/ -axiom some_remaining_axiom : Prop -``` - -### Property Test Tags - -Property tests must match Lean theorem names exactly: - -```solidity -/// Property: store_retrieve_roundtrip -function testProp_StoreRetrieveRoundtrip(...) public { ... } -``` - -Tag format: `/// Property: exact_theorem_name` - -### What NOT to Include - -❌ **Stale TODOs** in completed code -❌ **Difficulty estimates** after proof is done -❌ **Verbose explanations** of obvious code -❌ **Status claims** that don't match reality - -✅ **Brief descriptions** of what code does -✅ **Links** to related issues for context -✅ **Explanations** of non-obvious design choices -✅ **Accurate status** in module headers - -## Development - -**Layer 3 proofs**: Read [Compiler/Proofs/README.md](Compiler/Proofs/README.md), study completed proofs (`assign_equiv`, `storageStore_equiv`), use templates - -**New contracts**: Use `python3 scripts/generate_contract.py ` to scaffold all boilerplate files, then follow the `SimpleStorage` pattern: Spec → Invariants → Implementation → Proofs → Tests - -**Compiler changes**: Ensure `lake build` and `make check` pass - -## Adding Axioms - -Axioms should be **avoided whenever possible** as they introduce trust assumptions. If you must add an axiom: +A change to the trust boundary, an axiom, or CI updates +[docs/TRUST_ASSUMPTIONS.md](docs/TRUST_ASSUMPTIONS.md) and +[docs/AXIOMS.md](docs/AXIOMS.md) in the same PR. -1. **Exhaust all alternatives first**: - - Can you prove it? (even if difficult) - - Can you use a weaker lemma? - - Can you refactor to avoid the need? +## Proof hygiene -2. **Document in AXIOMS.md**: - - State the axiom clearly - - Provide soundness justification - - Explain why a proof isn't feasible - - Note future work to eliminate axiom - - Assess risk level +Enforced by CI; changing a rule means changing its script. -3. **Add inline comment in source**: - ```lean - /-- - AXIOM: Brief explanation - See AXIOMS.md for full soundness justification - -/ - axiom my_axiom : ... - ``` +1. No `sorry` (`scripts/check_lean_hygiene.py` double-checks the build). +2. No new axiom without a same-commit entry in docs/AXIOMS.md + (`scripts/check_axioms.py`). +3. Regenerate `PrintAxioms.lean` (`scripts/generate_print_axioms.py`) and + `artifacts/verification_status.json` (`make refresh-status`) when theorems + or counts change. +4. Proofs stay under 30 lines; over 50 needs an allowlist entry with a reason + (`scripts/check_proof_length.py`). +5. No `native_decide` outside smoke tests, and no `#eval`/`#check`/`#print` + in proof files. -4. **CI will validate**: The CI workflow automatically detects axioms and ensures they're documented in AXIOMS.md. Undocumented axioms will fail the build. +## Adding an axiom -See [AXIOMS.md](AXIOMS.md) for current axioms and detailed guidelines. +Avoid it: try to prove it, weaken the lemma, or refactor. If it is truly +needed, document it in docs/AXIOMS.md (statement, soundness justification, why +a proof is not feasible, risk, elimination path) and mark the declaration with +an `AXIOM:` doc comment pointing there. -## Key Files +## Adding a contract or intrinsic -- `Verity/` - EDSL, user-facing specs, and contract-specific specification proofs -- `Compiler/` - IR, Yul, codegen -- `Compiler/Proofs/` - Layer 2 & 3 proofs -- `docs/ROADMAP.md` - Progress tracking -- `test/` - Foundry tests +- Write contracts with `verity_contract`; reusable facets are `verity_mixin`s + that hosts `include`. `python3 scripts/generate_contract.py ` + scaffolds the files; `scripts/check_contract_structure.py` checks them. +- Refresh `test/property_manifest.json` + (`python3 scripts/extract_property_manifest.py`) and the counts + (`make refresh-status`, `python3 scripts/update_doc_numbers.py`). +- Intrinsics (`verity_intrinsic`) are consumer-owned opcode bindings; see + [docs/INTRINSICS.md](docs/INTRINSICS.md). +- Solidity imports: see [docs/SOLIDITY_IMPORT.md](docs/SOLIDITY_IMPORT.md) for + what a new supported construct must ship with. -Full details: [docs/VERIFICATION_STATUS.md](docs/VERIFICATION_STATUS.md) +Layer 3 proof work starts from [Compiler/Proofs/README.md](Compiler/Proofs/README.md). diff --git a/Compiler/CompilationModel/Compile.lean b/Compiler/CompilationModel/Compile.lean index 1160b38054..b8497a4e76 100644 --- a/Compiler/CompilationModel/Compile.lean +++ b/Compiler/CompilationModel/Compile.lean @@ -42,6 +42,14 @@ import Compiler.Yul.StatementRegions namespace Compiler.CompilationModel +/-- Nested if/else blocks cannot redeclare a surrounding Yul local, even in a +new block. Strictly smaller branch subtrees receive different numeric bases; +`pickFreshName` still protects against every source name in scope. -/ +def iteCondBaseName (thenBranch elseBranch : List Stmt) : String := + let count := Stmt.foldList (fun n _ _ => n + 1) 0 (thenBranch ++ elseBranch) + s!"__ite_cond_{count}" + + open Compiler open Compiler.Yul @@ -369,7 +377,7 @@ def compileStmtWithFork (fields : List Field) (events : List EventDef := []) -- Wrapped in block { } and freshened against names seen in this if/else shape -- so user locals cannot shadow the compiler-generated temp. let iteUsedNames := inScopeNames ++ collectExprNames cond ++ collectStmtListNames thenBranch ++ collectStmtListNames elseBranch - let iteCondName := pickFreshName "__ite_cond" iteUsedNames + let iteCondName := pickFreshName (iteCondBaseName thenBranch elseBranch) iteUsedNames pure [YulStmt.block [ YulStmt.let_ iteCondName condExpr, YulStmt.if_ (YulExpr.ident iteCondName) thenStmts, diff --git a/Compiler/CompileDriverCommon.lean b/Compiler/CompileDriverCommon.lean index a7893dcb84..97863bc792 100644 --- a/Compiler/CompileDriverCommon.lean +++ b/Compiler/CompileDriverCommon.lean @@ -675,7 +675,7 @@ def compileSpecsWithOptions IO.println s!" {spec.name}: {String.intercalate ", " primitives}" if !anyAxiomatized then IO.println " (no axiomatized primitives used)" - IO.println " Proof boundary: these primitives compile through explicit trusted boundaries (for example, keccak-backed hashing) and should be audited alongside AXIOMS.md/TRUST_ASSUMPTIONS.md." + IO.println " Proof boundary: these primitives compile through explicit trusted boundaries (for example, keccak-backed hashing) and should be audited alongside docs/AXIOMS.md and docs/TRUST_ASSUMPTIONS.md." IO.println "" IO.println "Local obligation report:" let mut anyLocalObligations := false diff --git a/Compiler/Linker.lean b/Compiler/Linker.lean index f420c4fcf9..c2e110731c 100644 --- a/Compiler/Linker.lean +++ b/Compiler/Linker.lean @@ -177,7 +177,7 @@ def loadLibrary (path : String) : IO (List LibraryFunction) := do **Note**: Library functions are injected at text level, not AST level. This is a known limitation (see issue #173). The formal proofs do not cover linked library code - this is a trust assumption documented in -`TRUST_ASSUMPTIONS.md`. +`docs/TRUST_ASSUMPTIONS.md`. -/ -- Indentation level for library functions in runtime code (3 levels × 4 spaces = 12 spaces) diff --git a/Compiler/Proofs/ArithmeticProfile.lean b/Compiler/Proofs/ArithmeticProfile.lean index 0577fe428c..7bd01ff179 100644 --- a/Compiler/Proofs/ArithmeticProfile.lean +++ b/Compiler/Proofs/ArithmeticProfile.lean @@ -163,7 +163,7 @@ theorem shr_bridge (shift value : Nat) : -- overflow checks. Contracts that need checked arithmetic must use the EDSL -- safeAdd/safeSub/safeMul functions, which return Option and revert on None. -- --- Cryptographic primitives: keccak256 is axiomatized (see AXIOMS.md). +-- Cryptographic primitives: keccak256 is axiomatized (see docs/AXIOMS.md). -- The mapping-slot derivation trusts the keccak FFI. -- -- Legacy/native bridge equivalence: comparison lemmas are part of the diff --git a/Compiler/Proofs/IRGeneration/Contract.lean b/Compiler/Proofs/IRGeneration/Contract.lean index dc61af6d21..81caeb5792 100644 --- a/Compiler/Proofs/IRGeneration/Contract.lean +++ b/Compiler/Proofs/IRGeneration/Contract.lean @@ -2046,7 +2046,7 @@ theorem compileFunctionSpec_correct_generic_with_helper_proofs_and_helper_ir_of_ /-- Primary whole-contract Layer 2 theorem: compilation preserves semantics for any supported `CompilationModel`. No contract-specific bridge premise. Layer 2 itself is axiom-free; the remaining documented project axiom is the -mapping-slot range assumption tracked in `AXIOMS.md`. -/ +mapping-slot range assumption tracked in `docs/AXIOMS.md`. -/ theorem compile_preserves_semantics (model : CompilationModel) (selectors : List Nat) diff --git a/Compiler/Proofs/IRGeneration/DenoteAgreement.lean b/Compiler/Proofs/IRGeneration/DenoteAgreement.lean index 9bb60c02b1..a453009713 100644 --- a/Compiler/Proofs/IRGeneration/DenoteAgreement.lean +++ b/Compiler/Proofs/IRGeneration/DenoteAgreement.lean @@ -36,7 +36,8 @@ def toRuntimeState (s : DenoteState) : SourceSemantics.RuntimeState := { world := s.world, immutable := s.immutable, bindings := s.bindings, selector := s.selector, externalCallOracle := fun n => ⟨s.externalCallSucceeded n, s.externalCallReturnValues n, s.externalCallPostWorld n⟩, - externalCallIndex := s.externalCallIndex } + externalCallIndex := s.externalCallIndex, + observedReturnWords := s.observedReturnWords } @[simp] theorem toRuntimeState_world (s : DenoteState) : (toRuntimeState s).world = s.world := rfl @@ -672,7 +673,8 @@ theorem execStmt_eq (fields : List Field) : · simp [toStmtResult, toRuntimeState, h, harity, hw, bindValues_eq, SourceSemantics.returndataAfterCall] · simp [toStmtResult, toRuntimeState, h] - | _, .returnValues .. | _, .returnArray .. | _, .returnBytes .. | _, .returnStorageWords .. + | _, .returnValues _ => by denote_stmt_arm + | _, .returnArray .. | _, .returnBytes .. | _, .returnStorageWords .. | _, .returnCodeData .. | _, .revertReturndata .. | _, .internalCall .. | _, .internalCallAssign .. | _, .rawLog .. | _, .unsafeBlock .. | _, .unsafeYul .. | _, .matchAdt .. => rfl diff --git a/Compiler/Proofs/IRGeneration/DenoteEquivalence.lean b/Compiler/Proofs/IRGeneration/DenoteEquivalence.lean index 725477a348..3d85185be2 100644 --- a/Compiler/Proofs/IRGeneration/DenoteEquivalence.lean +++ b/Compiler/Proofs/IRGeneration/DenoteEquivalence.lean @@ -99,7 +99,7 @@ theorem successResult_eq (spec : CompilationModel) (world : Verity.ContractState (ret : Option Nat) : toSourceResult (Denote.successResult sourceOracle spec world ret) = SourceSemantics.successResult spec world ret := - DenoteAgreement.toSourceResult_successResult spec world ret + DenoteAgreement.toSourceResult_successResult spec world ret [] theorem withTransactionContext_eq (world : Verity.ContractState) (tx : DenoteTransaction) : Denote.withTransactionContext world tx = diff --git a/Compiler/Proofs/IRGeneration/DenoteFunctionAgreement.lean b/Compiler/Proofs/IRGeneration/DenoteFunctionAgreement.lean index 8f34170deb..6090fe8865 100644 --- a/Compiler/Proofs/IRGeneration/DenoteFunctionAgreement.lean +++ b/Compiler/Proofs/IRGeneration/DenoteFunctionAgreement.lean @@ -179,8 +179,9 @@ theorem findDynamicArrayElementAtSlot_go_eq simp [toSourceResult, Denote.revertedResult, SourceSemantics.revertedResult] @[simp] theorem toSourceResult_successResult - (spec : CompilationModel) (world : Verity.ContractState) (ret : Option Nat) : - toSourceResult (Denote.successResult sourceOracle spec world ret) = + (spec : CompilationModel) (world : Verity.ContractState) (ret : Option Nat) + (returnWords : List Nat) : + toSourceResult (Denote.successResult sourceOracle spec world ret returnWords) = SourceSemantics.successResult spec world ret := by simp [toSourceResult, Denote.successResult, SourceSemantics.successResult] diff --git a/Compiler/Proofs/IRGeneration/Function.lean b/Compiler/Proofs/IRGeneration/Function.lean index c3c49b9d19..2283ad2712 100644 --- a/Compiler/Proofs/IRGeneration/Function.lean +++ b/Compiler/Proofs/IRGeneration/Function.lean @@ -3362,14 +3362,14 @@ theorem stmtListTerminalCore_compiledLegacyCompatible cases hcompile exact .block [YulStmt.let_ - (pickFreshName "__ite_cond" + (pickFreshName (iteCondBaseName thenBranch elseBranch) (compilerScope ++ (collectExprNames cond ++ (collectStmtListNames thenBranch ++ collectStmtListNames elseBranch)))) condIR, YulStmt.if_ (YulExpr.ident - (pickFreshName "__ite_cond" + (pickFreshName (iteCondBaseName thenBranch elseBranch) (compilerScope ++ (collectExprNames cond ++ (collectStmtListNames thenBranch ++ collectStmtListNames elseBranch))))) @@ -3377,7 +3377,7 @@ theorem stmtListTerminalCore_compiledLegacyCompatible YulStmt.if_ (YulExpr.call "iszero" [YulExpr.ident - (pickFreshName "__ite_cond" + (pickFreshName (iteCondBaseName thenBranch elseBranch) (compilerScope ++ (collectExprNames cond ++ (collectStmtListNames thenBranch ++ collectStmtListNames elseBranch))))]) diff --git a/Compiler/Proofs/IRGeneration/FunctionBody/Stmt.lean b/Compiler/Proofs/IRGeneration/FunctionBody/Stmt.lean index dd1b268d18..d562d92661 100644 --- a/Compiler/Proofs/IRGeneration/FunctionBody/Stmt.lean +++ b/Compiler/Proofs/IRGeneration/FunctionBody/Stmt.lean @@ -1534,7 +1534,7 @@ theorem compileStmt_terminal_ite_ok_inv CompilationModel.compileStmtList fields [] [] .calldata [] false inScopeNames [] elseBranch = Except.ok elseIR ∧ tempName = - CompilationModel.pickFreshName "__ite_cond" + CompilationModel.pickFreshName (CompilationModel.iteCondBaseName thenBranch elseBranch) (inScopeNames ++ collectExprNames cond ++ collectStmtListNames thenBranch ++ collectStmtListNames elseBranch) ∧ bodyIR = @@ -1563,7 +1563,7 @@ theorem compileStmt_terminal_ite_ok_inv simp [hcond, compileStmtListWithFork_cancun_eq_compileStmtList, hthen, helse, helseNonempty] at hcompile refine ⟨condIR, thenIR, elseIR, - CompilationModel.pickFreshName "__ite_cond" + CompilationModel.pickFreshName (CompilationModel.iteCondBaseName thenBranch elseBranch) (inScopeNames ++ collectExprNames cond ++ collectStmtListNames thenBranch ++ collectStmtListNames elseBranch), ?_, ?_, ?_, rfl, ?_⟩ @@ -1595,7 +1595,7 @@ theorem compileStmtList_terminal_ite_ok_inv (collectStmtBindNames (.ite cond thenBranch elseBranch) ++ inScopeNames) [] rest = Except.ok tailIR ∧ tempName = - CompilationModel.pickFreshName "__ite_cond" + CompilationModel.pickFreshName (CompilationModel.iteCondBaseName thenBranch elseBranch) (inScopeNames ++ collectExprNames cond ++ collectStmtListNames thenBranch ++ collectStmtListNames elseBranch) ∧ bodyIR = @@ -1770,20 +1770,20 @@ theorem compileStmtList_terminal_core_ok refine ⟨[YulStmt.block [ YulStmt.let_ - (CompilationModel.pickFreshName "__ite_cond" + (CompilationModel.pickFreshName (CompilationModel.iteCondBaseName thenBranch elseBranch) (inScopeNames ++ collectExprNames cond ++ collectStmtListNames thenBranch ++ collectStmtListNames elseBranch)) condIR, YulStmt.if_ (YulExpr.ident - (CompilationModel.pickFreshName "__ite_cond" + (CompilationModel.pickFreshName (CompilationModel.iteCondBaseName thenBranch elseBranch) (inScopeNames ++ collectExprNames cond ++ collectStmtListNames thenBranch ++ collectStmtListNames elseBranch))) thenIR, YulStmt.if_ (YulExpr.call "iszero" [YulExpr.ident - (CompilationModel.pickFreshName "__ite_cond" + (CompilationModel.pickFreshName (CompilationModel.iteCondBaseName thenBranch elseBranch) (inScopeNames ++ collectExprNames cond ++ collectStmtListNames thenBranch ++ collectStmtListNames elseBranch))]) elseIR @@ -7412,7 +7412,7 @@ theorem compiled_terminal_ite_temp_not_mem_scope {cond : Expr} {thenBranch elseBranch : List Stmt} (hincluded : scopeNamesIncluded scope inScopeNames) : - CompilationModel.pickFreshName "__ite_cond" + CompilationModel.pickFreshName (CompilationModel.iteCondBaseName thenBranch elseBranch) (inScopeNames ++ collectExprNames cond ++ collectStmtListNames thenBranch ++ collectStmtListNames elseBranch) ∉ scope := by apply pickFreshName_not_mem_scope_of_subset @@ -7430,7 +7430,7 @@ theorem bindingsExactlyMatchIRVarsOnScope_setCompiledTerminalIteTemp_irrelevant (hincluded : scopeNamesIncluded scope inScopeNames) : bindingsExactlyMatchIRVarsOnScope scope bindings (state.setVar - (CompilationModel.pickFreshName "__ite_cond" + (CompilationModel.pickFreshName (CompilationModel.iteCondBaseName thenBranch elseBranch) (inScopeNames ++ collectExprNames cond ++ collectStmtListNames thenBranch ++ collectStmtListNames elseBranch)) value) := by @@ -7862,7 +7862,7 @@ theorem exec_compileStmtList_terminal_core_sizeOf_extraFuel | nil => exfalso; exact stmtListTerminalCore_ne_nil helse rfl | cons => simp let tempName := - CompilationModel.pickFreshName "__ite_cond" + CompilationModel.pickFreshName (CompilationModel.iteCondBaseName thenBranch elseBranch) (inScopeNames ++ collectExprNames cond ++ collectStmtListNames thenBranch ++ collectStmtListNames elseBranch) refine ⟨[YulStmt.block diff --git a/Compiler/Proofs/IRGeneration/GenericInduction/Storage.lean b/Compiler/Proofs/IRGeneration/GenericInduction/Storage.lean index 862cc336c9..4866e791a9 100644 --- a/Compiler/Proofs/IRGeneration/GenericInduction/Storage.lean +++ b/Compiler/Proofs/IRGeneration/GenericInduction/Storage.lean @@ -7278,7 +7278,7 @@ theorem compiledStmtStep_ite | nil => exfalso; exact FunctionBody.stmtListTerminalCore_ne_nil helse rfl | cons => simp let tempName := - CompilationModel.pickFreshName "__ite_cond" + CompilationModel.pickFreshName (CompilationModel.iteCondBaseName thenBranch elseBranch) (scope ++ collectExprNames cond ++ collectStmtListNames thenBranch ++ collectStmtListNames elseBranch) let compiledIR := diff --git a/Compiler/Proofs/IRGeneration/IRStorageWord.lean b/Compiler/Proofs/IRGeneration/IRStorageWord.lean index d5fb6caa1e..541525002a 100644 --- a/Compiler/Proofs/IRGeneration/IRStorageWord.lean +++ b/Compiler/Proofs/IRGeneration/IRStorageWord.lean @@ -1,8 +1,8 @@ /- IRStorageWord: typed-storage helper surface for the IR storage carrier. - This module is **Phase 0** of the IR storage type refactor described in - `docs/IR_STORAGE_UINT256_REFACTOR.md`. It introduces a type alias plus + This module is **Phase 0** of the IR storage type refactor. It introduces a + type alias plus canonical injection / projection helpers and round-trip lemmas, *without* changing the underlying carrier or any existing callsite. @@ -22,9 +22,6 @@ `simpleStorageNativeStoreHitBridge` by reduction through the bounded carrier, then drop the corresponding hypotheses from `simpleStorage_endToEnd_native_evmYulLean`. - - The full plan and acceptance signals live in - `docs/IR_STORAGE_UINT256_REFACTOR.md`. -/ import EvmYul.UInt256 diff --git a/Compiler/Proofs/IRGeneration/SourceSemantics.lean b/Compiler/Proofs/IRGeneration/SourceSemantics.lean index 713dae8670..252513868e 100644 --- a/Compiler/Proofs/IRGeneration/SourceSemantics.lean +++ b/Compiler/Proofs/IRGeneration/SourceSemantics.lean @@ -850,6 +850,10 @@ structure RuntimeState where selector : Nat := 0 externalCallOracle : Nat → ExternalCallOutcome := fun _ => ⟨false, [], none⟩ externalCallIndex : Nat := 0 + /-- Words produced by `Stmt.returnValues`, in source order. + Mirrors `DenoteState.observedReturnWords`. Proof semantics observes the + words, not a Solidity panic payload: `Stmt.panic` remains a revert. -/ + observedReturnWords : Option (List Nat) := none inductive StmtResult where | continue (state : RuntimeState) @@ -2822,6 +2826,11 @@ mutual externalCallIndex := state.externalCallIndex + 1 } else .revert | none => .revert + | state, .returnValues values => + match evalExprList fields state values with + | some resolved => + .stop { state with observedReturnWords := some (resolved.map wordNormalize) } + | none => .revert | _, _ => .revert def execStmtListWithEvents (fields : List Field) (events : List EventDef) : @@ -3200,6 +3209,11 @@ mutual externalCallIndex := state.externalCallIndex + 1 } else .revert | none => .revert + | state, .returnValues values => + match evalExprList fields state values with + | some resolved => + .stop { state with observedReturnWords := some (resolved.map wordNormalize) } + | none => .revert | _, _ => .revert def execStmtList (fields : List Field) : RuntimeState → List Stmt → StmtResult @@ -4826,6 +4840,14 @@ mutual externalCallIndex := state.externalCallIndex + 1 } else .revert | none => .revert + | .returnValues values => + -- Base expression semantics, matching `execStmt`. Arguments are not + -- given helper fuel; the slice importer inlines pure helpers before + -- this statement is produced. + match evalExprList fields state values with + | some resolved => + .stop { state with observedReturnWords := some (resolved.map wordNormalize) } + | none => .revert | _ => .revert termination_by stmt => (fuel, sizeOf stmt) decreasing_by all_goals (simp_wf; omega) diff --git a/Compiler/Proofs/IRGeneration/SupportedSpec.lean b/Compiler/Proofs/IRGeneration/SupportedSpec.lean index 25c072e7ec..360439408b 100644 --- a/Compiler/Proofs/IRGeneration/SupportedSpec.lean +++ b/Compiler/Proofs/IRGeneration/SupportedSpec.lean @@ -3240,7 +3240,7 @@ structure SupportedFunction (spec : CompilationModel) (fn : FunctionSpec) where /-- `nonreentrant(lockField)` guards sit outside the proven fragment: the TLOAD/TSTORE prologue injected by `attachNonReentrantGuard` is not yet modelled by the source semantics. This makes the documented boundary - (TRUST_ASSUMPTIONS.md) machine-checked instead of prose-only. -/ + (docs/TRUST_ASSUMPTIONS.md) machine-checked instead of prose-only. -/ noNonReentrant : fn.nonReentrantLock = none params : SupportedParamProfile fn.params returns : SupportedReturnProfile fn diff --git a/Compiler/Proofs/MappingSlot.lean b/Compiler/Proofs/MappingSlot.lean index 935f3a3c55..6eda5180e7 100644 --- a/Compiler/Proofs/MappingSlot.lean +++ b/Compiler/Proofs/MappingSlot.lean @@ -61,7 +61,7 @@ def solidityMappingSlot (baseSlot key : Nat) : Nat := `keccak256(abi.encode(key, baseSlot))`. **Not** injectivity of keccak256 on arbitrary `ByteArray`s (256-bit - output, infinite domain). See `AXIOMS.md`. -/ + output, infinite domain). See `docs/AXIOMS.md`. -/ axiom solidityMappingSlot_injective (base₁ key₁ base₂ key₂ : Nat) : solidityMappingSlot base₁ key₁ = solidityMappingSlot base₂ key₂ → diff --git a/Compiler/Proofs/README.md b/Compiler/Proofs/README.md index 7771589ee2..cf47f0bf6f 100644 --- a/Compiler/Proofs/README.md +++ b/Compiler/Proofs/README.md @@ -3,7 +3,7 @@ Formal verification proofs for the Verity compiler pipeline: `EDSL -> CompilationModel -> IR -> native EVMYulLean`. -See [TRUST_ASSUMPTIONS.md](../../TRUST_ASSUMPTIONS.md) for the full trust boundary and [veritylang.com/verification](https://veritylang.com/verification) for proof status. +See [TRUST_ASSUMPTIONS.md](../../docs/TRUST_ASSUMPTIONS.md) for the full trust boundary and [veritylang.com/verification](https://veritylang.com/verification) for proof status. ## Verification Layers diff --git a/Compiler/Proofs/Storage/MappingCoherentAllKeys.lean b/Compiler/Proofs/Storage/MappingCoherentAllKeys.lean index 0c4ee75789..deae41a11b 100644 --- a/Compiler/Proofs/Storage/MappingCoherentAllKeys.lean +++ b/Compiler/Proofs/Storage/MappingCoherentAllKeys.lean @@ -21,7 +21,7 @@ Trust: the aligned `writeMap*` laws inherit `solidityMappingSlot_injective` (ABI mapping-preimage collision resistance), exactly as the per-channel globals in `MappingCoherence` - already do — see AXIOMS.md. No new axiom is introduced here. The + already do — see docs/AXIOMS.md. No new axiom is introduced here. The map-vs-nested-map cross case additionally takes an explicit `MappingBasesNotDerived` layout certificate; it is a hypothesis of the same shape as the image-avoidance `∀` that diff --git a/Compiler/Proofs/YulGeneration/PatchRulesProofs.lean b/Compiler/Proofs/YulGeneration/PatchRulesProofs.lean index 10ed026be9..6e26cc6321 100644 --- a/Compiler/Proofs/YulGeneration/PatchRulesProofs.lean +++ b/Compiler/Proofs/YulGeneration/PatchRulesProofs.lean @@ -91,9 +91,4 @@ def foundation_patch_pack_obligations (eval : EvalExpr) : Prop := div_one_right_preserves eval ∧ mod_one_right_preserves eval -/-- Placeholder object-level proof hook until object semantics preservation is stated directly. - External parity packs define their own object-rule obligations using this hook. -/ -def ObjectPatchPreserves (_rule : ObjectPatchRule) : Prop := - True - end Compiler.Proofs.YulGeneration.PatchRulesProofs diff --git a/Compiler/Selectors.lean b/Compiler/Selectors.lean index 8f01a0f8b9..37e0fcf967 100644 --- a/Compiler/Selectors.lean +++ b/Compiler/Selectors.lean @@ -16,7 +16,7 @@ defense-in-depth consistency check. ## References - Solidity ABI Spec: https://docs.soliditylang.org/en/latest/abi-spec.html#function-selector -- Trust Assumptions: TRUST_ASSUMPTIONS.md +- Trust Assumptions: docs/TRUST_ASSUMPTIONS.md -/ namespace Compiler diff --git a/Compiler/SolidityImport/Access.lean b/Compiler/SolidityImport/Access.lean new file mode 100644 index 0000000000..86dd19c820 --- /dev/null +++ b/Compiler/SolidityImport/Access.lean @@ -0,0 +1,172 @@ +import Compiler.SolidityImport.Coverage +import Verity.Core.NarrowTypes +import Verity.Core.Address + +/-! +Typed access to an imported model: calling a function and reading a storage +member. `solidity_import` generates a named, typed wrapper around each of these +(`example.f`, `example.position.credit`), so specifications do not name slots, +bindings or packing. +-/ + +open Compiler.CompilationModel Compiler.CompilationModel.Denote +open Verity.Core +namespace Compiler.CompilationModel.SolidityImport + +/-- A Solidity value carried in one 256-bit word. -/ +class Word (α : Type) where + toWord : α → Nat + ofWord : Nat → α + +instance : Word Uint256 := ⟨Uint256.val, Uint256.ofNat⟩ +instance : Word (UIntN bits) := ⟨UIntN.val, UIntN.ofNat bits⟩ +instance : Word Address := ⟨Address.val, Address.ofNat⟩ +instance : Word (BytesN 32) := ⟨BytesN.val, BytesN.ofNat 32⟩ +instance : Word Bool := ⟨fun b => if b then 1 else 0, fun n => n != 0⟩ + +@[simp] theorem toWord_uint256 (x : Uint256) : Word.toWord x = x.val := rfl +@[simp] theorem toWord_uintN (x : UIntN bits) : Word.toWord x = x.val := rfl +@[simp] theorem toWord_address (x : Address) : Word.toWord x = x.val := rfl +@[simp] theorem toWord_bytes32 (x : BytesN 32) : Word.toWord x = x.val := rfl +@[simp] theorem ofWord_uint256 (n : Nat) : (Word.ofWord n : Uint256) = Uint256.ofNat n := rfl +@[simp] theorem ofWord_uintN (n : Nat) : (Word.ofWord n : UIntN bits) = UIntN.ofNat bits n := rfl + +/-- Body of the function `name` in `model`; `[]` if there is none. -/ +def functionBody (model : CompilationModel) (name : String) : List Stmt := + go model.functions +where + go : List FunctionSpec → List Stmt + | [] => [] + | fn :: rest => if fn.name = name then fn.body else go rest + +/-- Run the function `name` of an imported model in `world` with the given +argument words; `none` means it reverted. -/ +def runFunction (oracle : DenoteOracle) (model : CompilationModel) (name : String) + (world : Verity.ContractState) (args : Env) : Option (List Nat) := + match execStmtList oracle model.fields { world, bindings := args } (functionBody model name) with + | .stop state | .continue state | .return _ state => state.observedReturnWords + | .revert => none + +/-- `field[keys].member` in `world`, read exactly as the imported model reads +it: solc's slot, word offset and packing. `0` if there is no such member. -/ +def readMember (oracle : DenoteOracle) (model : CompilationModel) (world : Verity.ContractState) + (field : String) (keys : List Nat) (member : String) : Nat := + match keys with + | [k] => (evalExpr oracle model.fields { world, bindings := [("key1", k)] } + (.structMember field (.param "key1") member)).getD 0 + | [k1, k2] => (evalExpr oracle model.fields { world, bindings := [("key1", k1), ("key2", k2)] } + (.structMember2 field (.param "key1") (.param "key2") member)).getD 0 + | _ => 0 + +/-- The layout entry of `field[..].member`, if the model has one. -/ +def findMember (model : CompilationModel) (field member : String) : Option StructMember := + (findStructMembers model.fields field).bind (findStructMember · member) + +/-- Bit width of `field[..].member`: its packed width, else a full word. -/ +def memberWidth (model : CompilationModel) (field member : String) : Nat := + match findMember model field member with + | some { packed := some packed, .. } => packed.width + | _ => 256 + +private theorem masked_lt (x : Uint256) (packed : PackedBits) : + (Uint256.and x (Uint256.ofNat (packedMaskNat packed))).val < 2 ^ packed.width := by + have hle : (Uint256.and x (Uint256.ofNat (packedMaskNat packed))).val ≤ packedMaskNat packed := + Nat.le_trans (Nat.mod_le _ _) (Nat.le_trans Nat.and_le_right (Nat.mod_le _ _)) + have hlt : (Uint256.and x (Uint256.ofNat (packedMaskNat packed))).val < 2 ^ 256 := + (Uint256.and x _).isLt + generalize (Uint256.and x (Uint256.ofNat (packedMaskNat packed))).val = v at hle hlt + have hpos : 0 < 2 ^ packed.width := Nat.pow_pos (by decide) + unfold packedMaskNat at hle + split at hle + · exact Nat.lt_of_lt_of_le hlt (Nat.pow_le_pow_right (by decide) (by omega)) + · omega + +/-- A member read fits in the member's width. -/ +theorem readMember_lt (oracle : DenoteOracle) (model : CompilationModel) + (world : Verity.ContractState) (field : String) (keys : List Nat) (member : String) : + readMember oracle model world field keys member < 2 ^ memberWidth model field member := by + unfold readMember memberWidth findMember + split + all_goals first + | exact Nat.pow_pos (by decide) + | skip + all_goals + simp only [evalExpr, lookupValue, List.find?, bind, Option.bind] + split <;> try exact Nat.pow_pos (by decide) + rename_i fieldInfo slot members _ hmembers + simp only [hmembers] + split <;> try exact Nat.pow_pos (by decide) + rename_i m hm + simp only [hm] + cases m with + | mk name ty wordOffset packed => + cases packed with + | none => exact (readFieldWord _ _ _).isLt + | some packed => exact masked_lt _ packed + +/-- A member read of width `bits` is exactly representable as `UIntN bits`. -/ +theorem val_uintN_readMember (oracle : DenoteOracle) (model : CompilationModel) + (world : Verity.ContractState) (field : String) (keys : List Nat) (member : String) + (h : memberWidth model field member = bits) : + (UIntN.ofNat bits (readMember oracle model world field keys member)).val = + readMember oracle model world field keys member := + Nat.mod_eq_of_lt (h ▸ readMember_lt oracle model world field keys member) + +/-- A full-word member read is exactly representable as `Uint256`. -/ +theorem val_uint256_readMember (oracle : DenoteOracle) (model : CompilationModel) + (world : Verity.ContractState) (field : String) (keys : List Nat) (member : String) + (h : memberWidth model field member = 256) : + (Uint256.ofNat (readMember oracle model world field keys member)).val = + readMember oracle model world field keys member := by + have hlt := readMember_lt oracle model world field keys member + rw [h] at hlt + exact Nat.mod_eq_of_lt hlt + +/-- The model's own read of `field[a][b].member`, with keys bound to `a` and `b`, +is `readMember` at their values. -/ +theorem evalExpr_structMember2_param (oracle : DenoteOracle) (model : CompilationModel) + (state : DenoteState) (field a b member : String) + (h : (findFieldWithResolvedSlot model.fields field).isSome ∧ + (findMember model field member).isSome) : + evalExpr oracle model.fields state (.structMember2 field (.param a) (.param b) member) = + some (readMember oracle model state.world field + [lookupValue state.bindings a, lookupValue state.bindings b] member) := by + obtain ⟨hf, hm⟩ := h + unfold findMember at hm + simp only [readMember, evalExpr, bind, Option.bind] + cases hfs : findFieldWithResolvedSlot model.fields field with + | none => simp [hfs] at hf + | some fs => + cases hms : findStructMembers model.fields field with + | none => simp [hms] at hm + | some ms => + cases hmm : findStructMember ms member with + | none => simp [hms, hmm] at hm + | some m => + simp only [hmm, lookupValue] + cases m.packed <;> simp + +/-- The model's own read of `field[a].member`, with the key bound to `a`, is +`readMember` at its value. -/ +theorem evalExpr_structMember_param (oracle : DenoteOracle) (model : CompilationModel) + (state : DenoteState) (field a member : String) + (h : (findFieldWithResolvedSlot model.fields field).isSome ∧ + (findMember model field member).isSome) : + evalExpr oracle model.fields state (.structMember field (.param a) member) = + some (readMember oracle model state.world field [lookupValue state.bindings a] member) := by + obtain ⟨hf, hm⟩ := h + unfold findMember at hm + simp only [readMember, evalExpr, bind, Option.bind] + cases hfs : findFieldWithResolvedSlot model.fields field with + | none => simp [hfs] at hf + | some fs => + cases hms : findStructMembers model.fields field with + | none => simp [hms] at hm + | some ms => + cases hmm : findStructMember ms member with + | none => simp [hms, hmm] at hm + | some m => + simp only [hmm, lookupValue] + cases m.packed <;> simp + +end Compiler.CompilationModel.SolidityImport diff --git a/Compiler/SolidityImport/Coverage.lean b/Compiler/SolidityImport/Coverage.lean new file mode 100644 index 0000000000..9fe47a0c12 --- /dev/null +++ b/Compiler/SolidityImport/Coverage.lean @@ -0,0 +1,400 @@ +import Verity.Core.Model.Denote + +/-! +# Denotation coverage certificate for Solidity slices + +The Solidity-slice importer (`Compiler/SolidityImport`) emits only a small, +explicitly listed fragment of the deep `CompilationModel`. This module is the +machine-checked side of that contract: + +* `exprCovered` / `stmtCovered` / `stmtListCovered` are the decidable + whitelists of `Expr` / `Stmt` constructors the importer may emit. A + constructor is whitelisted only when `Compiler.CompilationModel.Denote` + gives it a dedicated arm — never the catch-all `none` / `.revert` fallback. +* For every whitelisted constructor, an `*_arm` theorem pins the exact + denotation equation by `rfl`. If the corresponding `Denote` arm is removed + or its meaning changes, the pin stops compiling, so the whitelist cannot + silently drift away from the semantics. + +The importer refuses to register a slice whose body fails `stmtListCovered`, +and registers the passing verdict as a theorem proved by `decide`. + +This predicate is deliberately *not* `SupportedFunction`: that predicate is +the Denote↔IR raccord boundary (it rejects `panic`, `returnValues`, ...), +while this one characterizes the compiler-free `Denote` execution path only. +-/ + +namespace Compiler.CompilationModel.SolidityImport + +open Compiler.CompilationModel +open Compiler.CompilationModel.Denote + +/-! ## Expression whitelist -/ + +/-- Expressions the slice importer may emit, mirrored against +`Denote.evalExpr`. Every `true` case has a dedicated arm there; every other +constructor (including `paramDynamicMember*`, `paramDynamicStaticComposite`, +`mulDiv512*`, raw calls, `internalCall`, `intrinsic`, `forkIfAtLeast`, ADTs, +`arrayElementWord`, `mappingChain`, and any future constructor) is rejected. -/ +def exprCovered : Expr → Bool + | .literal _ => true + | .param _ => true + | .localVar _ => true + | .blockTimestamp => true + | .structMember _ key _ => exprCovered key + | .structMember2 _ key1 key2 _ => exprCovered key1 && exprCovered key2 + | .add a b | .sub a b | .mul a b | .div a b + | .lt a b | .gt a b | .le a b | .ge a b | .eq a b + | .bitAnd a b | .bitXor a b => exprCovered a && exprCovered b + | .logicalNot a => exprCovered a + | _ => false + +/-- Every expression in the list is covered. -/ +def exprListCovered : List Expr → Bool + | [] => true + | e :: es => exprCovered e && exprListCovered es + +/-! ## Statement whitelist -/ + +mutual + +/-- Statements the slice importer may emit, mirrored against `Denote.execStmt`. +Multi-value returns are covered: `Stmt.returnValues` records the resolved words +in `DenoteState.observedReturnWords` and finishes with `.stop`. -/ +def stmtCovered : Stmt → Bool + | .letVar _ value => exprCovered value + | .assignVar _ value => exprCovered value + | .ite cond thenBranch elseBranch => + exprCovered cond && stmtListCovered thenBranch && stmtListCovered elseBranch + | .panic _ => true + | .returnValues args => exprListCovered args + | _ => false + +/-- Every statement in the list is covered. -/ +def stmtListCovered : List Stmt → Bool + | [] => true + | s :: ss => stmtCovered s && stmtListCovered ss + +end + +/-! ## Arm pins: expressions + +Each pin restates the dedicated `Denote.evalExpr` arm. They are proved by +`rfl`, so deleting the arm (falling back to the catch-all `none`) or changing +the arm's meaning breaks the build here. -/ + +theorem evalExpr_literal_arm (oracle : DenoteOracle) (fields : List Field) + (s : DenoteState) (n : Nat) : + evalExpr oracle fields s (.literal n) = some (wordNormalize n) := rfl + +theorem evalExpr_param_arm (oracle : DenoteOracle) (fields : List Field) + (s : DenoteState) (name : String) : + evalExpr oracle fields s (.param name) = some (lookupValue s.bindings name) := rfl + +theorem evalExpr_localVar_arm (oracle : DenoteOracle) (fields : List Field) + (s : DenoteState) (name : String) : + evalExpr oracle fields s (.localVar name) = some (lookupValue s.bindings name) := rfl + +theorem evalExpr_blockTimestamp_arm (oracle : DenoteOracle) (fields : List Field) + (s : DenoteState) : + evalExpr oracle fields s .blockTimestamp = some s.world.blockTimestamp.val := rfl + +theorem evalExpr_add_arm (oracle : DenoteOracle) (fields : List Field) + (s : DenoteState) (a b : Expr) : + evalExpr oracle fields s (.add a b) = + (do + let lhs : Verity.Core.Uint256 := ← evalExpr oracle fields s a + let rhs : Verity.Core.Uint256 := ← evalExpr oracle fields s b + pure (lhs + rhs).val) := rfl + +theorem evalExpr_sub_arm (oracle : DenoteOracle) (fields : List Field) + (s : DenoteState) (a b : Expr) : + evalExpr oracle fields s (.sub a b) = + (do + let lhs : Verity.Core.Uint256 := ← evalExpr oracle fields s a + let rhs : Verity.Core.Uint256 := ← evalExpr oracle fields s b + pure (lhs - rhs).val) := rfl + +theorem evalExpr_mul_arm (oracle : DenoteOracle) (fields : List Field) + (s : DenoteState) (a b : Expr) : + evalExpr oracle fields s (.mul a b) = + (do + let lhs : Verity.Core.Uint256 := ← evalExpr oracle fields s a + let rhs : Verity.Core.Uint256 := ← evalExpr oracle fields s b + pure (lhs * rhs).val) := rfl + +theorem evalExpr_div_arm (oracle : DenoteOracle) (fields : List Field) + (s : DenoteState) (a b : Expr) : + evalExpr oracle fields s (.div a b) = + (do + let lhs : Verity.Core.Uint256 := ← evalExpr oracle fields s a + let rhs : Verity.Core.Uint256 := ← evalExpr oracle fields s b + pure (lhs / rhs).val) := rfl + +theorem evalExpr_lt_arm (oracle : DenoteOracle) (fields : List Field) + (s : DenoteState) (a b : Expr) : + evalExpr oracle fields s (.lt a b) = + (do + let lhs ← evalExpr oracle fields s a + let rhs ← evalExpr oracle fields s b + pure (boolWord (decide (lhs < rhs)))) := rfl + +theorem evalExpr_gt_arm (oracle : DenoteOracle) (fields : List Field) + (s : DenoteState) (a b : Expr) : + evalExpr oracle fields s (.gt a b) = + (do + let lhs ← evalExpr oracle fields s a + let rhs ← evalExpr oracle fields s b + pure (boolWord (decide (rhs < lhs)))) := rfl + +theorem evalExpr_le_arm (oracle : DenoteOracle) (fields : List Field) + (s : DenoteState) (a b : Expr) : + evalExpr oracle fields s (.le a b) = + (do + let lhs ← evalExpr oracle fields s a + let rhs ← evalExpr oracle fields s b + pure (boolWord (decide (lhs ≤ rhs)))) := rfl + +theorem evalExpr_ge_arm (oracle : DenoteOracle) (fields : List Field) + (s : DenoteState) (a b : Expr) : + evalExpr oracle fields s (.ge a b) = + (do + let lhs ← evalExpr oracle fields s a + let rhs ← evalExpr oracle fields s b + pure (boolWord (decide (rhs ≤ lhs)))) := rfl + +theorem evalExpr_eq_arm (oracle : DenoteOracle) (fields : List Field) + (s : DenoteState) (a b : Expr) : + evalExpr oracle fields s (.eq a b) = + (do + let lhs ← evalExpr oracle fields s a + let rhs ← evalExpr oracle fields s b + pure (boolWord (decide (lhs = rhs)))) := rfl + +theorem evalExpr_bitAnd_arm (oracle : DenoteOracle) (fields : List Field) + (s : DenoteState) (a b : Expr) : + evalExpr oracle fields s (.bitAnd a b) = + (do + let lhs ← evalExpr oracle fields s a + let rhs ← evalExpr oracle fields s b + pure (Verity.Core.Uint256.and lhs rhs).val) := rfl + +theorem evalExpr_bitXor_arm (oracle : DenoteOracle) (fields : List Field) + (s : DenoteState) (a b : Expr) : + evalExpr oracle fields s (.bitXor a b) = + (do + let lhs ← evalExpr oracle fields s a + let rhs ← evalExpr oracle fields s b + pure (Verity.Core.Uint256.xor lhs rhs).val) := rfl + +/-- `Expr.logicalOr` and `Expr.logicalAnd` are eager in `evalExpr`, so they are +not slice constructors. `Expr.paramDynamicHeadWord` is an ABI-head read and is +excluded from this certificate; the slice does not claim public ABI decoding. -/ +theorem evalExpr_logicalNot_arm (oracle : DenoteOracle) (fields : List Field) + (s : DenoteState) (a : Expr) : + evalExpr oracle fields s (.logicalNot a) = + (do + let value ← evalExpr oracle fields s a + pure (boolWord (decide (value = 0)))) := rfl + +/-- Struct member reads through a single mapping key are denoted, including +packed sub-word slicing from the field's declared `StructMember` metadata. -/ +theorem evalExpr_structMember_arm (oracle : DenoteOracle) (fields : List Field) + (s : DenoteState) (field : String) (key : Expr) (memberName : String) : + evalExpr oracle fields s (.structMember field key memberName) = + (do + let keyVal ← evalExpr oracle fields s key + match findFieldWithResolvedSlot fields field, findStructMembers fields field with + | some (fieldInfo, slot), some members => + match findStructMember members memberName with + | some member => + let targetSlot := wordNormalize + (oracle.mappingSlot slot keyVal + member.wordOffset) + let rawWord := (readFieldWord s.world fieldInfo targetSlot).val + match member.packed with + | none => some rawWord + | some packed => + some (Verity.Core.Uint256.and + (Verity.Core.Uint256.shr packed.offset rawWord) + (packedMaskNat packed)).val + | none => none + | _, _ => none) := rfl + +/-- Struct member reads through two mapping keys are denoted. -/ +theorem evalExpr_structMember2_arm (oracle : DenoteOracle) (fields : List Field) + (s : DenoteState) (field : String) (key1 key2 : Expr) (memberName : String) : + evalExpr oracle fields s (.structMember2 field key1 key2 memberName) = + (do + let key1Val ← evalExpr oracle fields s key1 + let key2Val ← evalExpr oracle fields s key2 + match findFieldWithResolvedSlot fields field, findStructMembers fields field with + | some (fieldInfo, slot), some members => + match findStructMember members memberName with + | some member => + let innerSlot := oracle.mappingSlot slot key1Val + let outerSlot := oracle.mappingSlot innerSlot key2Val + let targetSlot := wordNormalize (outerSlot + member.wordOffset) + let rawWord := (readFieldWord s.world fieldInfo targetSlot).val + match member.packed with + | none => some rawWord + | some packed => + some (Verity.Core.Uint256.and + (Verity.Core.Uint256.shr packed.offset rawWord) + (packedMaskNat packed)).val + | none => none + | _, _ => none) := rfl + +/-! ## Arm pins: statements -/ + +theorem execStmt_letVar_arm (oracle : DenoteOracle) (fields : List Field) + (s : DenoteState) (name : String) (value : Expr) : + execStmt oracle fields s (.letVar name value) = + (match evalExpr oracle fields s value with + | some resolved => .continue { s with bindings := bindValue s.bindings name resolved } + | none => .revert) := rfl + +theorem execStmt_assignVar_arm (oracle : DenoteOracle) (fields : List Field) + (s : DenoteState) (name : String) (value : Expr) : + execStmt oracle fields s (.assignVar name value) = + (match evalExpr oracle fields s value with + | some resolved => .continue { s with bindings := bindValue s.bindings name resolved } + | none => .revert) := rfl + +/-- `Stmt.ite` is lazy in the denotation: only the taken branch executes, so +guards inside an untaken branch cannot introduce a spurious revert. -/ +theorem execStmt_ite_arm (oracle : DenoteOracle) (fields : List Field) + (s : DenoteState) (cond : Expr) (thenBranch elseBranch : List Stmt) : + execStmt oracle fields s (.ite cond thenBranch elseBranch) = + (match evalExpr oracle fields s cond with + | some condVal => + if condVal != 0 then + execStmtList oracle fields s thenBranch + else + execStmtList oracle fields s elseBranch + | none => .revert) := rfl + +theorem execStmt_panic_arm (oracle : DenoteOracle) (fields : List Field) + (s : DenoteState) (code : Verity.Core.PanicCode) : + execStmt oracle fields s (.panic code) = .revert := rfl + +/-- Multi-value returns are denoted: the resolved words are recorded in source +order in `observedReturnWords` and execution stops. -/ +theorem execStmt_returnValues_arm (oracle : DenoteOracle) (fields : List Field) + (s : DenoteState) (args : List Expr) : + execStmt oracle fields s (.returnValues args) = + (match evalExprList oracle fields s args with + | some resolved => + .stop { s with observedReturnWords := some (resolved.map wordNormalize) } + | none => .revert) := rfl + +/-- A `returnValues` statement terminates the statement list: later statements +(a `panic` here) never execute, and the recorded words survive in source +order. -/ +theorem execStmtList_returnValues_stop_arm (oracle : DenoteOracle) (fields : List Field) + (s : DenoteState) (a b : Nat) : + execStmtList oracle fields s + [.returnValues [.literal a, .literal b], .panic .arithmeticOverflow] = + .stop { s with observedReturnWords := some [wordNormalize a, wordNormalize b] } := by + simp [execStmtList, execStmt, evalExprList, evalExpr, wordNormalize] + +/-- Every function body of a model is in the slice whitelist. -/ +def modelImportCovered (model : CompilationModel) : Bool := + model.functions.all fun fn => stmtListCovered fn.body + +/-- Level A: run a body on scalar bindings. Arguments are Solidity words, not +ABI-decoded calldata. `some` is the source-order return list of a successful +run; `none` is a revert. The panic payload is not returned. -/ +def denoteScalarBody + (oracle : DenoteOracle) (fields : List Field) + (world : Verity.ContractState) (timestamp : Nat) + (bindings : Env) (body : List Stmt) : Option (List Nat) := + match execStmtList oracle fields + { world := { world with blockTimestamp := Verity.Core.Uint256.ofNat timestamp } + bindings := bindings } body with + | .stop state | .continue state | .return _ state => state.observedReturnWords + | .revert => none + +private theorem boolAnd2 {a b : Bool} (h : (a && b) = true) : a = true ∧ b = true := by + cases a <;> cases b <;> simp_all + +private theorem boolAnd3 {a b c : Bool} (h : (a && b && c) = true) : + b = true ∧ c = true := by + cases a <;> cases b <;> cases c <;> simp_all + +/-- `next.world = origin.world` whenever execution continues, stops, or returns. +A revert has no successor state. -/ +def preservesWorld (origin : DenoteState) : StmtOutcome → Prop + | .continue next | .stop next | .return _ next => next.world = origin.world + | .revert => True + +mutual + +theorem execStmt_slice_world + (oracle : DenoteOracle) (fields : List Field) (state : DenoteState) (stmt : Stmt) + (h : stmtCovered stmt = true) : + preservesWorld state (execStmt oracle fields state stmt) := by + unfold stmtCovered at h + split at h + · rename_i name value + cases hEval : evalExpr oracle fields state value <;> + simp [preservesWorld, execStmt, hEval] + · rename_i name value + cases hEval : evalExpr oracle fields state value <;> + simp [preservesWorld, execStmt, hEval] + · rename_i cond thenBranch elseBranch + have ⟨hthen, helse⟩ := boolAnd3 h + cases hEval : evalExpr oracle fields state cond + · simp [preservesWorld, execStmt, hEval] + · rename_i bit + by_cases hbit : (bit != 0) = true + · simpa [preservesWorld, execStmt, hEval, hbit] using + execStmtList_slice_world oracle fields state thenBranch hthen + · have hnot : (bit != 0) = false := by simpa using hbit + simpa [preservesWorld, execStmt, hEval, hnot] using + execStmtList_slice_world oracle fields state elseBranch helse + · simp [preservesWorld, execStmt] + · rename_i args + cases hEval : evalExprList oracle fields state args <;> + simp [preservesWorld, execStmt, hEval] + · cases h + +theorem execStmtList_slice_world + (oracle : DenoteOracle) (fields : List Field) (state : DenoteState) + (stmts : List Stmt) (h : stmtListCovered stmts = true) : + preservesWorld state (execStmtList oracle fields state stmts) := by + match stmts with + | [] => simp [preservesWorld, execStmtList] + | stmt :: rest => + unfold stmtListCovered at h + have ⟨hstmt, hrest⟩ := boolAnd2 h + have hone := execStmt_slice_world oracle fields state stmt hstmt + simp only [execStmtList] + cases hExec : execStmt oracle fields state stmt + · rename_i next + have hworld : next.world = state.world := by + simpa [preservesWorld, hExec] using hone + have htail := execStmtList_slice_world oracle fields next rest hrest + simpa [preservesWorld, hworld] using htail + · rename_i next + simpa [preservesWorld, hExec] using hone + · rename_i value next + simpa [preservesWorld, hExec] using hone + · simp [preservesWorld] + +end + +/-- A covered slice does not write persistent storage. A revert leaves the +caller's pre-state in place because `denoteScalarBody` discards it. -/ +theorem execStmtList_slice_storageWords + (oracle : DenoteOracle) (fields : List Field) + (world : Verity.ContractState) (timestamp : Nat) + (bindings : Env) (body : List Stmt) + (h : stmtListCovered body = true) : + let state : DenoteState := + { world := { world with blockTimestamp := Verity.Core.Uint256.ofNat timestamp } + bindings := bindings } + preservesWorld state (execStmtList oracle fields state body) := by + intro state + exact execStmtList_slice_world oracle fields state body h + +end Compiler.CompilationModel.SolidityImport diff --git a/Compiler/SolidityImport/Differential.lean b/Compiler/SolidityImport/Differential.lean new file mode 100644 index 0000000000..df7ee77e73 --- /dev/null +++ b/Compiler/SolidityImport/Differential.lean @@ -0,0 +1,127 @@ +import Compiler.SolidityImport.Coverage +import Compiler.SolidityImport.Report +import Compiler.CompilationModel +import Compiler.Codegen +import Compiler.Yul.PrettyPrint +import Compiler.Keccak.Sponge +import Lean.Data.Json + +/-! Test-only runner for an arbitrary covered Solidity slice. The execution path +is the same `Denote.execStmtList` used by proofs. JSON/IO and the concrete hash +implementation are harness code, not additional proof assumptions. -/ +namespace Compiler.CompilationModel.SolidityImport.Differential +open Lean Compiler.CompilationModel.Denote Verity.Core + +private def word (j : Json) : Except String Nat := do + let s ← j.getStr? + let some n := s.toNat? | throw "expected an unsigned decimal word" + unless n < 2^256 do throw "word exceeds uint256" + return n + +private def words (j : Json) : Except String (List Nat) := do + (← j.getArr?).toList.mapM word + +private def pairs (j : Json) : Except String (List (Nat × Nat)) := do + (← j.getArr?).toList.mapM fun row => do + let xs ← words row + match xs with + | [slot, value] => return (slot, value) + | _ => throw "storage row must contain slot and value" + +private def mappingSlot (base key : Nat) : Nat := + let bytes := fun n : Nat => ByteArray.mk ((List.range 32).toArray.map fun i => + UInt8.ofNat (n / 2^(8*(31-i)) % 256)) + (KeccakEngine.keccak256 (bytes key ++ bytes base)).data.foldl + (fun n b => n * 256 + b.toNat) 0 + +private def oracle : DenoteOracle := + { mappingSlot, keccakMemorySlice := fun _ _ _ => 0 } + +private def jsonWords (xs : List Nat) : Json := toJson (xs.map toString) + +private def execute (model : CompilationModel) (fn : FunctionSpec) (j : Json) : Except String Json := do + let ident ← j.getObjValAs? String "id" + let args ← words (← j.getObjVal? "args") + unless args.length == fn.params.length do throw "model argument count mismatch" + let storage ← pairs (← j.getObjVal? "storage") + let slots ← words (← j.getObjVal? "observe") + unless (storage.map Prod.fst).eraseDups.length == storage.length do + throw "duplicate initial storage slot" + let timestamp ← word (← j.getObjVal? "timestamp") + let world := Verity.defaultState.withStorageWords fun slot => + match slot with + | .slot n => Uint256.ofNat ((storage.find? (fun p => p.1 == n)).map Prod.snd |>.getD 0) + | _ => Verity.defaultState.storageWords slot + let initial : DenoteState := + { world := { world with blockTimestamp := Uint256.ofNat timestamp } + bindings := fn.params.map (·.name) |>.zip args } + let result := execStmtList oracle model.fields initial fn.body + let (status, output, finalWorld) ← match result with + | .stop final => match final.observedReturnWords with + | some xs => pure ("ok", xs, final.world) + | none => throw "covered slice stopped without return words" + | .revert => pure ("revert", [], initial.world) + | _ => throw "covered slice did not return or revert" + return Json.mkObj [("id", toJson ident), ("status", toJson status), + ("words", jsonWords output), + ("storage", jsonWords (slots.map fun slot => (finalWorld.storageWords (.slot slot)).val))] + +/-- Compile one root on its own with the ordinary Verity compiler, behind a +placeholder selector. Success means the compiler accepts the function; it says +nothing about agreement with Denote or solc (that is what `run` samples). -/ +def compileRoot (model : CompilationModel) (fn : FunctionSpec) := + Compiler.CompilationModel.compile { model with functions := [fn] } [0x12345678] .osaka + +/-- The four statuses of every root, one block per root. `importable` holds for +every listed root; `denoteCovered` and `compilerProofCovered` come from the +import report; `compilable` is this compile attempt. -/ +def statusText (model : CompilationModel) (report : ImportReport) : String := Id.run do + let mut lines := #[] + for fn in model.functions do + let some st := report.functions.find? (·.function == fn.name) + | lines := lines.push s!"function {fn.name}\n missing from the import report" + let compilable := match compileRoot model fn with + | .ok _ => "true" + | .error reason => s!"false ({reason.replace "\n" " "})" + lines := lines.push (String.intercalate "\n" + [s!"function {fn.name}", " importable true", s!" denoteCovered {st.denoteCovered}", + s!" compilable {compilable}", s!" compilerProofCovered {st.compilerProof.toText}"]) + return String.intercalate "\n" lines.toList ++ "\n" + +private def selectRoot (model : CompilationModel) (name : String) : IO FunctionSpec := do + let some fn := model.functions.find? (·.name == name) + | throw (IO.userError s!"no imported root {name}; roots: {model.functions.map (·.name)}") + return fn + +/-- The driver imports the model, then invokes this shared entrypoint. +`describe` also attempts the ordinary Verity compiler; a failure is explicit. -/ +def run (model : CompilationModel) (report : ImportReport) (args : List String) : IO UInt32 := do + unless modelImportCovered model do throw (IO.userError "unsupported Denote slice") + match args with + | ["status", output] => IO.FS.writeFile output (statusText model report) + | ["describe", name, output, yulPath] => + let fn ← selectRoot model name + let (compilable, reason) ← match compileRoot model fn with + | .ok ir => + IO.FS.writeFile yulPath (Compiler.Yul.render (Compiler.emitYul ir)) + pure (true, "") + | .error reason => pure (false, reason) + let projections := (report.projections.filter (·.function == name)).map fun p => Json.mkObj + [("parameter", toJson p.parameter), ("member", toJson p.member), ("modelParam", toJson p.modelParam)] + IO.FS.writeFile output (Json.compress (Json.mkObj + [("function", toJson name), ("params", toJson (fn.params.map (·.name))), + ("projections", toJson projections), + ("digest", toJson report.sourceDigest), ("settings", toJson report.settingsJson), + ("compilable", toJson compilable), ("compileError", toJson reason), + ("panicPayloadObserved", toJson report.observesPanicPayload)])) + | ["run", name, input, output, digest] => + let fn ← selectRoot model name + unless digest == report.sourceDigest do throw (IO.userError "source/importer digest changed; rebuild the campaign") + let parsed ← IO.ofExcept (Json.parse (← IO.FS.readFile input)) + let results ← IO.ofExcept do + (← parsed.getArr?).toList.mapM (execute model fn) + IO.FS.writeFile output (Json.compress (toJson results)) + | _ => throw (IO.userError "expected status OUTPUT, describe FUNCTION OUTPUT YUL, or run FUNCTION INPUT OUTPUT DIGEST") + return 0 + +end Compiler.CompilationModel.SolidityImport.Differential diff --git a/Compiler/SolidityImport/Import.lean b/Compiler/SolidityImport/Import.lean new file mode 100644 index 0000000000..b81cef6eb0 --- /dev/null +++ b/Compiler/SolidityImport/Import.lean @@ -0,0 +1,1500 @@ +import Lean +import Compiler.Sha256.Engine +import Compiler.SolidityImport.Access +import Compiler.SolidityImport.Profile +import Compiler.SolidityImport.Quote +import Compiler.SolidityImport.Report + +/-! +Solidity importer. Pinned `solc --standard-json` supplies the AST, declaration +ids, and `storageLayout`. The `solidity_import` command selects a function, +closes over the definitions that resolution actually reaches, and elaborates a +`CompilationModel`. It does not interpret that model: execution is +`Denote.execStmt`, restricted by `stmtListCovered`. + +```lean +solidity_import midnight from "vendor/midnight" entry "src/Midnight.sol" + using { evmVersion := "osaka", viaIR := true, optimizerRuns := some 466, bytecodeHash := "none" } + contract Midnight + function updatePositionView(Market, bytes32, address) +``` + +This defines `midnight.model`, `midnight.report`, `midnight.sourceDigest`, the +theorem `midnight.covered`, and typed accessors: `midnight.updatePositionView` +for each imported function and `midnight.position.credit` for each storage +struct member. + +The frontend is in the trust base. A covered model is not, by itself, a proof +that the model matches the Solidity source or solc's bytecode. +-/ + +open Lean Meta Elab Command + +namespace Compiler.CompilationModel.SolidityImport + +def importerVersion : String := "solidity-import-1" + +private def solcLongVersion := "0.8.34+commit.80d5c536" + +private def officialSolcSha256s : Array String := #[ + "d40adc6f9fdbb22a97d32a02fa05688bf2ee7886affc48c9851b0afd4a726b39", + "0a2829292697dda542e4e365bb63fbd6d3ed51537140222a880ab760cffa7746"] + +private def acceptedSolcBanners : Array String := #[ + s!"solc, the solidity compiler commandline interface\nVersion: {solcLongVersion}.Linux.g++", + s!"solc, the solidity compiler commandline interface\nVersion: {solcLongVersion}.Darwin.appleclang"] + +private def field (j : Json) (key : String) : MetaM Json := + match j.getObjVal? key with + | .ok v => pure v + | .error e => throwError "{e}" + +private def field? (j : Json) (key : String) : Option Json := + (j.getObjVal? key).toOption + +private def str (j : Json) : MetaM String := + match j.getStr? with + | .ok v => pure v + | .error e => throwError "{e}" + +private def arr (j : Json) : MetaM (Array Json) := + match j.getArr? with + | .ok v => pure v + | .error e => throwError "{e}" + +private def bool (j : Json) : MetaM Bool := + match j.getBool? with + | .ok v => pure v + | .error e => throwError "{e}" + +private def flexNat (j : Json) : MetaM Nat := do + match j.getNat? with + | .ok n => pure n + | .error _ => + match j.getStr? with + | .ok s => + match s.toNat? with + | some n => pure n + | none => throwError "not a nat: {s}" + | .error e => throwError "{e}" + +private def nodeKind (j : Json) : MetaM String := do + str (← field j "nodeType") + +private def typeString (j : Json) : MetaM String := do + str (← field (← field j "typeDescriptions") "typeString") + +private def optStr (j : Json) (key : String) : Option String := + match field? j key with + | some v => v.getStr?.toOption + | none => none + +private def hexDigit (n : Nat) : Char := + if n < 10 then Char.ofNat ('0'.toNat + n) else Char.ofNat ('a'.toNat + n - 10) + +private def sha256Hex (bytes : ByteArray) : String := + (Sha256Engine.sha256 bytes).data.foldl (init := "") fun acc byte => + acc.push (hexDigit (byte.toNat / 16)) |>.push (hexDigit (byte.toNat % 16)) + +/-- A lowered expression and the statements that must run before it. -/ +private structure Val where + pre : Array Stmt + expr : Expr + +private inductive SPath where + | one (field : String) (key : Expr) + | two (field : String) (k1 k2 : Expr) + | outer (field : String) (k1 : Expr) + +private inductive Ref where + | expr (v : Val) + | path (pre : Array Stmt) (p : SPath) + | mem (id : Nat) (pre : Array Stmt) + | state (name : String) (pre : Array Stmt) + +private structure MemParam where + param : String + structName : String + members : Array (String × String) + +private structure FieldInfo where + slot : Nat + field : Field + keyCount : Nat + memberNames : Array String + opaqueNames : Array String + +private structure FnRec where + contract : String + name : String + declId : Nat + paramTypes : Array String + +private structure ProjRec where + parameter : String + member : String + structName : String + headWord : Nat + modelParam : String + +private structure OpaqRec where + field : String + name : String + solcType : String + wordOffset : Nat + byteOffset : Nat + +private structure SrcParam where + name : String + id : Nat + +private structure Env where + sourceNames : List String + nodeFile : RBMap Nat String compare + files : RBMap String ByteArray compare + funs : RBMap Nat Json compare + funContract : RBMap Nat String compare + structs : RBMap Nat Json compare + stateVars : RBMap Nat Json compare + values : RBMap Nat Expr compare + paths : RBMap Nat SPath compare + mems : RBMap Nat MemParam compare + fieldsByName : RBMap String FieldInfo compare + layoutItems : RBMap String Json compare + layoutTypes : Json + included : Array FnRec + projections : Array ProjRec + opaqueMembers : Array OpaqRec + referenced : Array String + scalarTy : RBMap Nat ParamType compare + next : Nat + /-- Every binding name allocated in the current root, including its parameters. -/ + bound : List String + stack : List Nat + yulNames : RBMap String Expr compare + currentFile : String + +private def Env.init : Env where + sourceNames := [] + nodeFile := RBMap.empty + files := RBMap.empty + funs := RBMap.empty + funContract := RBMap.empty + structs := RBMap.empty + stateVars := RBMap.empty + values := RBMap.empty + paths := RBMap.empty + mems := RBMap.empty + fieldsByName := RBMap.empty + layoutItems := RBMap.empty + layoutTypes := Json.null + included := #[] + projections := #[] + opaqueMembers := #[] + referenced := #[] + scalarTy := RBMap.empty + next := 0 + bound := [] + stack := [] + yulNames := RBMap.empty + currentFile := "" + +private abbrev M := StateT Env MetaM + +private def mField (j : Json) (key : String) : M Json := liftM (field j key) +private def mStr (j : Json) : M String := liftM (str j) +private def mArr (j : Json) : M (Array Json) := liftM (arr j) +private def mBool (j : Json) : M Bool := liftM (bool j) +private def mNat (j : Json) : M Nat := liftM (flexNat j) +private def mKind (j : Json) : M String := liftM (nodeKind j) +private def mType (j : Json) : M String := liftM (typeString j) + +private def failAt (j : Json) (why : String) : M α := do + let env ← get + let frames := env.stack.reverse.map fun id => + let owner := env.funContract.find? id |>.getD "" + let name := (env.funs.find? id).bind (fun fn => optStr fn "name") |>.getD s!"decl#{id}" + s!"{owner}.{name}" + let why := s!"[solidity-import:unsupported] {why}\nclosure: {String.intercalate " -> " frames}" + let file := + match (field? j "id").bind (fun id => id.getNat?.toOption) with + | some id => env.nodeFile.find? id |>.getD env.currentFile + | none => env.currentFile + let some bytes := env.files.find? file | throwError "{file}: {why}" + let some src := (field? j "src").bind (fun s => s.getStr?.toOption) + | throwError "{file}: {why}" + let pieces := src.splitOn ":" + let some start := pieces[0]?.bind (·.toNat?) | throwError "{file}: {why}" + let some size := pieces[1]?.bind (·.toNat?) | throwError "{file}: {why}" + let before := bytes.data.extract 0 (min start bytes.size) + let (line, column) := before.foldl + (fun (p : Nat × Nat) b => if b == 10 then (p.1 + 1, 1) else (p.1, p.2 + 1)) (1, 1) + let excerptBytes : ByteArray := + ⟨bytes.data.extract start (min (start + size) (min bytes.size (start + 120)))⟩ + let excerpt := String.fromUTF8? excerptBytes |>.getD "" + let kind := (field? j "nodeType").bind (fun k => k.getStr?.toOption) |>.getD "node" + throwError "{file}:{line}:{column}: {kind}: {why}\n{excerpt}" + +private def refInt (j : Json) : M Int := do + match field? j "overloadedDeclarations" with + | some over => + let xs ← mArr over + if xs.size ≠ 0 then failAt j "ambiguous declaration" + | none => pure () + match (← mField j "referencedDeclaration").getInt? with + | .ok i => pure i + | .error e => failAt j e + +private def fresh : M String := do + let env ← get + let projected := env.mems.toList.flatMap fun (_, p) => + p.members.toList.map (fun (member, _) => s!"{p.param}_{member}") + let reserved := env.sourceNames ++ projected ++ env.bound + -- The prefix is a valid compiler identifier, but is not assumed reserved by + -- Solidity. Check every source name and every potential scalar projection. + for _ in [:reserved.length + 1] do + let n := (← get).next + modify fun e => { e with next := e.next + 1 } + let candidate := s!"_verity_slice_tmp_{n}" + unless reserved.contains candidate do + modify fun e => { e with bound := candidate :: e.bound } + return candidate + throwError "unable to allocate a hygienic slice binding" + +private def yulKeywords : List String := + ["let", "if", "switch", "case", "default", "for", "break", "continue", "leave", + "function", "true", "false"] + +/-- Binding for the Solidity local `name`: the source name itself when it is +free, so that proofs can refer to it, else `name_1`, `name_2`, ... Names that +the compiler reserves or Yul forbids fall back to `fresh`. -/ +private def freshFor (name : String) : M String := do + if name.startsWith "__" || name.startsWith "_verity_slice_tmp" then return ← fresh + let env ← get + let projected := env.mems.toList.flatMap fun (_, p) => + p.members.toList.map (fun (member, _) => s!"{p.param}_{member}") + let usable (candidate : String) : Bool := + !(env.bound.contains candidate || projected.contains candidate || + yulKeywords.contains candidate || (Verity.Core.Intrinsics.yulBuiltinArity? candidate).isSome) + let mut chosen := none + if usable name then chosen := some name + else + for k in [1:env.bound.length + 2] do + let candidate := s!"{name}_{k}" + if chosen.isNone && usable candidate && !env.sourceNames.contains candidate then + chosen := some candidate + let some binding := chosen | fresh + modify fun e => { e with bound := binding :: e.bound } + pure binding + +private def isAtom : Expr → Bool + | .literal _ | .localVar _ | .param _ | .blockTimestamp => true + | _ => false + +private def atom (v : Val) : M Val := do + if v.pre.isEmpty && isAtom v.expr then + pure v + else + let n ← fresh + pure { pre := v.pre.push (.letVar n v.expr), expr := .localVar n } + +private def bitsOf (ty : String) : Option Nat := + if ty == "uint256" || ty == "uint" then some 256 + else if ty == "bytes32" then some 256 + else if ty == "address" then some 160 + else if ty.startsWith "uint" then (ty.drop 4).toNat? + else none + +private def paramType (ty : String) : Option ParamType := + match ty with + | "uint256" | "uint" => some .uint256 + | "address" => some .address + | "bytes32" => some .bytes32 + | "bool" => some .bool + | _ => + match bitsOf ty with + | some n => if n != 256 && ty.startsWith "uint" then some (.uintN n) else none + | none => none + +private def noteFn (fn : Json) : M Unit := do + let id ← mNat (← mField fn "id") + let env ← get + unless env.included.any (·.declId == id) do + let name ← mStr (← mField fn "name") + let params ← mArr (← mField (← mField fn "parameters") "parameters") + let mut tys : Array String := #[] + for p in params do + tys := tys.push (← mType p) + let contract := env.funContract.find? id |>.getD "" + modify fun e => + let added := e.included.push ⟨contract, name, id, tys⟩ + { e with included := added } + +private def markField (name : String) : M Unit := do + let env ← get + unless env.referenced.contains name do + modify fun e => { e with referenced := e.referenced.push name } + +private def noteProjection (p : ProjRec) : M Unit := do + let env ← get + unless env.projections.any (fun q => q.parameter == p.parameter && q.member == p.member) do + modify fun e => { e with projections := e.projections.push p } + +private def mappingKey (ty : String) : MetaM MappingKeyType := + match ty with + | "t_bytes32" => pure .bytes32 + | "t_address" => pure .address + | "t_uint256" => pure .uint256 + | _ => throwError "unsupported mapping key {ty}" + +private def layoutType (types : Json) (id : String) : MetaM Json := + field types id + +/-- Solidity names of the mapping keys of the state variable `name` +(`mapping(bytes32 id => ...)`), outermost first; `""` when unnamed. -/ +private def mappingKeyNames (env : Env) (name : String) : List String := + let decl := env.stateVars.toList.find? fun (_, j) => optStr j "name" == some name + let rec go (fuel : Nat) (ty : Json) : List String := + match fuel with + | 0 => [] + | fuel + 1 => + if optStr ty "nodeType" == some "Mapping" then + (optStr ty "keyName" |>.getD "") :: (match field? ty "valueType" with + | some v => go fuel v + | none => []) + else [] + match decl.bind (fun (_, j) => field? j "typeName") with + | some ty => go 8 ty + | none => [] + +private def buildField (types item : Json) : M FieldInfo := do + let name ← mStr (← mField item "label") + let slot ← mNat (← mField item "slot") + let typeId ← mStr (← mField item "type") + let top ← liftM (layoutType types typeId) + unless (← mStr (← mField top "encoding")) == "mapping" do + throwError "storage field {name} is not a mapping" + let key1 ← liftM (mappingKey (← mStr (← mField top "key"))) + let valueId ← mStr (← mField top "value") + let value ← liftM (layoutType types valueId) + let encoding ← mStr (← mField value "encoding") + let (key2, structTy) ← + if encoding == "mapping" then + let key2 ← liftM (mappingKey (← mStr (← mField value "key"))) + let innerId ← mStr (← mField value "value") + pure (some key2, ← liftM (layoutType types innerId)) + else if encoding == "inplace" then + pure (none, value) + else + throwError "unsupported mapping value encoding {encoding} for {name}" + let members ← mArr (← mField structTy "members") + let mut members' : Array StructMember := #[] + let mut names : Array String := #[] + let mut skipped : Array String := #[] + for m in members do + let label ← mStr (← mField m "label") + let solcType ← mStr (← mField m "type") + let word ← mNat (← mField m "slot") + let byteOff ← mNat (← mField m "offset") + if solcType.startsWith "t_array" || solcType.startsWith "t_mapping" then + skipped := skipped.push label + modify fun e => + let item : OpaqRec := ⟨name, label, solcType, word, byteOff⟩ + { e with opaqueMembers := e.opaqueMembers.push item } + else if solcType.startsWith "t_uint" then + let some bits := (solcType.drop 6).toNat? | throwError "bad uint {solcType}" + let bitOff := byteOff * 8 + unless bitOff + bits ≤ 256 do throwError "packed field {label} does not fit in a word" + let packed : Option PackedBits := + if bits == 256 && bitOff == 0 then none else some { offset := bitOff, width := bits } + let ty : StructMemberType := if bits == 16 then .uint16 else .uint256 + members' := members'.push { name := label, ty, wordOffset := word, packed } + names := names.push label + else + throwError "unsupported layout member {label} : {solcType}" + let ty : FieldType := + match key2 with + | some k2 => .mappingStruct2 key1 k2 members'.toList + | none => .mappingStruct key1 members'.toList + let field : Field := { name, ty, slot := some slot } + pure { slot, field, keyCount := if key2.isSome then 2 else 1, + memberNames := names, opaqueNames := skipped } + +/-- Resolve only reached fields: an unrelated unsupported layout must not +prevent importing a supported function closure. -/ +private def resolveField (name : String) (at_ : Json) : M FieldInfo := do + if let some info := (← get).fieldsByName.find? name then return info + let env ← get + let some item := env.layoutItems.find? name | failAt at_ s!"no storage layout for {name}" + let info ← try buildField env.layoutTypes item catch ex => + failAt at_ (← ex.toMessageData.toString) + modify fun e => { e with fieldsByName := e.fieldsByName.insert name info } + return info + +private def memberRead (pre : Array Stmt) (path : SPath) (member : String) (at_ : Json) : M Val := do + let (fieldName, read) ← match path with + | .one field key => pure (field, Expr.structMember field key member) + | .two field k1 k2 => pure (field, Expr.structMember2 field k1 k2 member) + | .outer _ _ => failAt at_ "member access on an incomplete mapping" + let some info := (← get).fieldsByName.find? fieldName + | failAt at_ s!"no storage layout for {fieldName}" + if info.opaqueNames.contains member then + failAt at_ s!"member {member} is opaque in this slice" + unless info.memberNames.contains member do + failAt at_ s!"member {member} is not a layout member of {fieldName}" + markField fieldName + pure { pre, expr := read } + +/-- `if cond then [yes] else [no]` with single-statement branches. -/ +private def iteStmt (cond : Expr) (yes no : Stmt) : Stmt := + .ite cond [yes] [no] + +private def overflowPanic : Stmt := .panic .arithmeticOverflow + +private def divPanic : Stmt := .panic .divisionByZero + +mutual + +private partial def lowerYul (j : Json) : M Expr := do + match ← mKind j with + | "YulIdentifier" => + let name ← mStr (← mField j "name") + match (← get).yulNames.find? name with + | some expr => pure expr + | none => failAt j s!"unbound Yul identifier {name}" + | "YulFunctionCall" => + let fname ← mStr (← mField (← mField j "functionName") "name") + let args ← mArr (← mField j "arguments") + let mut xs : Array Expr := #[] + for arg in args do + xs := xs.push (← lowerYul arg) + match fname, xs with + | "xor", #[a, b] => pure (.bitXor a b) + | "mul", #[a, b] => pure (.mul a b) + | "lt", #[a, b] => pure (.lt a b) + | _, _ => failAt j s!"unsupported Yul builtin {fname}" + | kind => failAt j s!"unsupported Yul node {kind}" + +private partial def lowerExpr (j : Json) : M Val := do + match ← mKind j with + | "Literal" => + let raw := optStr j "value" |>.getD "" + let some n := raw.toNat? | failAt j s!"unsupported literal {raw}" + pure { pre := #[], expr := .literal n } + | "TupleExpression" => + if ← mBool (← mField j "isInlineArray") then failAt j "inline arrays are outside this slice" + let cs ← mArr (← mField j "components") + if cs.size == 1 then + if cs[0]!.isNull then failAt j "empty tuple component" + lowerExpr cs[0]! + else + failAt j "a multi-value expression is only valid as a return" + | "BinaryOperation" => lowerBinary j + | "Conditional" => lowerConditional j + | "FunctionCall" => lowerCall j + | "Identifier" | "MemberAccess" | "IndexAccess" => + match ← lowerRef j with + | .expr v => pure v + | _ => failAt j "storage or memory path used as a value" + | kind => failAt j s!"unsupported expression {kind}" + +private partial def lowerRef (j : Json) : M Ref := do + match ← mKind j with + | "Identifier" => + let id ← refInt j + if id < 0 then failAt j "unresolved builtin identifier" + let n := id.toNat + let env ← get + if let some expr := env.values.find? n then + pure (.expr { pre := #[], expr }) + else if let some path := env.paths.find? n then + pure (.path #[] path) + else if env.mems.contains n then + pure (.mem n #[]) + else if let some decl := env.stateVars.find? n then + let name ← mStr (← mField decl "name") + if let some item := env.layoutItems.find? name then + unless (← mNat (← mField item "astId")) == n do + failAt j s!"shadowed storage declaration {name} is outside this slice" + pure (.state name #[]) + else + failAt j s!"unresolved identifier {id}" + | "IndexAccess" => + let base ← lowerRef (← mField j "baseExpression") + let key ← atom (← lowerExpr (← mField j "indexExpression")) + match base with + | .state name pre => + let info ← resolveField name j + markField name + if info.keyCount == 2 then + pure (.path (pre ++ key.pre) (.outer name key.expr)) + else + pure (.path (pre ++ key.pre) (.one name key.expr)) + | .path pre (.outer field k1) => + pure (.path (pre ++ key.pre) (.two field k1 key.expr)) + | _ => failAt j "index of a non-mapping" + | "MemberAccess" => + let member ← mStr (← mField j "memberName") + let base ← mField j "expression" + if member == "timestamp" && (← mKind base) == "Identifier" && + optStr base "name" == some "block" && + optStr ((field? base "typeDescriptions").getD Json.null) "typeIdentifier" == some "t_magic_block" then + pure (.expr { pre := #[], expr := .blockTimestamp }) + else if member == "max" then + lowerTypeMax j base + else + match ← lowerRef base with + | .path pre path => + pure (.expr (← memberRead pre path member j)) + | .mem id pre => + pure (.expr (← projectMember id pre member j)) + | _ => failAt j s!"unsupported member {member}" + | kind => failAt j s!"unsupported reference {kind}" + +private partial def lowerTypeMax (at_ base : Json) : M Ref := do + unless (← mKind base) == "FunctionCall" do + failAt at_ "max is only supported on type(T)" + let callee ← mField base "expression" + unless (← mKind callee) == "Identifier" && optStr callee "name" == some "type" do + failAt at_ "max is only supported on type(T)" + let args ← mArr (← mField base "arguments") + unless args.size == 1 do failAt at_ "type() expects one argument" + let arg := args[0]! + unless (← mKind arg) == "ElementaryTypeNameExpression" do + failAt at_ "type() expects an elementary type" + let tname ← mStr (← mField (← mField arg "typeName") "name") + let some bits := bitsOf tname | failAt at_ s!"unsupported type().max {tname}" + pure (.expr { pre := #[], expr := .literal (2 ^ bits - 1) }) + +private partial def projectMember (id : Nat) (pre : Array Stmt) (member : String) (at_ : Json) : M Val := do + let some mem := (← get).mems.find? id | failAt at_ "unknown memory parameter" + let some idx := mem.members.findIdx? (fun p => p.1 == member) + | failAt at_ s!"{member} is not a member of {mem.structName}" + let mut head : Nat := 0 + for i in [:idx] do + let ty := mem.members[i]!.2 + if ty.endsWith "[]" then + head := head + 1 + else if ty.startsWith "struct " || ty.any (· == '[') then + failAt at_ s!"cannot place {member}; earlier member {mem.members[i]!.1} is {ty}" + else + head := head + 1 + let modelParam := s!"{mem.param}_{member}" + noteProjection { + parameter := mem.param, member := member, structName := mem.structName, + headWord := head, modelParam := modelParam } + pure { pre, expr := .param modelParam } + +private partial def lowerBinary (j : Json) : M Val := do + let op ← mStr (← mField j "operator") + let left ← lowerExpr (← mField j "leftExpression") + let right ← lowerExpr (← mField j "rightExpression") + let common ← mStr (← mField (← mField j "commonType") "typeString") + unless (bitsOf common).isSome || common == "bool" do + failAt j s!"unsupported operand type {common}" + match op with + | "+" => + let some bits := bitsOf common | failAt j s!"unsupported add type {common}" + checkedAdd bits left right + | "-" => checkedSub left right + | "*" => + let some bits := bitsOf common | failAt j s!"unsupported mul type {common}" + checkedMul bits left right + | "/" => checkedDiv left right + | "<" => cmp .lt left right + | ">" => cmp .gt left right + | "<=" => cmp .le left right + | ">=" => cmp .ge left right + | "==" => cmp .eq left right + | "!=" => + let v ← cmp .eq left right + pure { v with expr := .logicalNot v.expr } + | _ => failAt j s!"unsupported operator {op}" + +private partial def checkedSub (left right : Val) : M Val := do + let a ← atom left + let b ← atom right + let dest ← fresh + let ok := Stmt.assignVar dest (.sub a.expr b.expr) + let ite := iteStmt (.lt a.expr b.expr) overflowPanic ok + pure { pre := a.pre ++ b.pre |>.push (.letVar dest (.literal 0)) |>.push ite, expr := .localVar dest } + +private partial def checkedDiv (left right : Val) : M Val := do + let a ← atom left + let b ← atom right + let dest ← fresh + let ok := Stmt.assignVar dest (.div a.expr b.expr) + let ite := iteStmt (.eq b.expr (.literal 0)) divPanic ok + pure { pre := a.pre ++ b.pre |>.push (.letVar dest (.literal 0)) |>.push ite, expr := .localVar dest } + +private partial def checkedAdd (bits : Nat) (left right : Val) : M Val := do + let a ← atom left + let b ← atom right + let dest ← fresh + let sum := Expr.add a.expr b.expr + let ok := Stmt.assignVar dest sum + let cond := + if bits == 256 then Expr.lt sum a.expr + else Expr.lt (.literal (2 ^ bits - 1)) sum + pure { pre := a.pre ++ b.pre |>.push (.letVar dest (.literal 0)) |>.push (iteStmt cond overflowPanic ok), + expr := .localVar dest } + +private partial def checkedMul (bits : Nat) (left right : Val) : M Val := do + let a ← atom left + let b ← atom right + let dest ← fresh + let prod := Expr.mul a.expr b.expr + let ok := Stmt.assignVar dest prod + -- Check the EVM-width product before the Solidity-width bound. For e.g. + -- uint248, an overflowing 256-bit product can wrap below the uint248 bound. + let bounded := if bits == 256 then ok + else iteStmt (.lt (.literal (2 ^ bits - 1)) prod) overflowPanic ok + let inner := iteStmt (.eq (.div prod a.expr) b.expr) bounded overflowPanic + let ite := iteStmt (.eq a.expr (.literal 0)) bounded inner + pure { pre := a.pre ++ b.pre |>.push (.letVar dest (.literal 0)) |>.push ite, expr := .localVar dest } + +private partial def cmp (op : Expr → Expr → Expr) (left right : Val) : M Val := do + let a ← atom left + let b ← atom right + pure { pre := a.pre ++ b.pre, expr := op a.expr b.expr } + +private partial def lowerConditional (j : Json) : M Val := do + let cond ← atom (← lowerExpr (← mField j "condition")) + let yes ← lowerExpr (← mField j "trueExpression") + let no ← lowerExpr (← mField j "falseExpression") + let dest ← fresh + let thenB := yes.pre.push (.assignVar dest yes.expr) + let elseB := no.pre.push (.assignVar dest no.expr) + let ite := Stmt.ite cond.expr thenB.toList elseB.toList + pure { pre := cond.pre.push (.letVar dest (.literal 0)) |>.push ite, expr := .localVar dest } + +private partial def lowerCast (j : Json) : M Val := do + let targetExpr ← mField j "expression" + unless (← mKind targetExpr) == "ElementaryTypeNameExpression" do + failAt j "unsupported cast" + let tname ← mStr (← mField (← mField targetExpr "typeName") "name") + let some bits := bitsOf tname | failAt j s!"unsupported cast target {tname}" + let args ← mArr (← mField j "arguments") + unless args.size == 1 do failAt j "cast expects one argument" + let v ← lowerExpr args[0]! + let src := ← mType args[0]! + let narrow : Bool := + match bitsOf src with + | some sb => decide (sb > bits) + | none => decide (bits < 256) && !src.startsWith "int_const" + if narrow then + let a ← atom v + let dest ← fresh + let bound := Stmt.letVar dest (.bitAnd a.expr (.literal (2 ^ bits - 1))) + pure { pre := a.pre.push bound, expr := .localVar dest } + else + pure v + +private partial def lowerCall (j : Json) : M Val := do + let names ← mArr (← mField j "names") + unless names.isEmpty do failAt j "named call arguments are outside this slice" + let kind ← mStr (← mField j "kind") + if kind == "typeConversion" then + lowerCast j + else if kind == "functionCall" then + let callee ← mField j "expression" + let (fnId, receiver?) ← match ← mKind callee with + | "MemberAccess" => + let id ← refInt callee + if id < 0 then failAt callee "builtin call is outside this slice" + let base ← mField callee "expression" + let baseTy ← mType base + if baseTy.startsWith "type(library " then + pure (id.toNat, none) + else + pure (id.toNat, some base) + | "Identifier" => + let id ← refInt callee + if id < 0 then failAt callee "builtin call is outside this slice" + pure (id.toNat, none) + | _ => failAt callee "unsupported callee" + let args ← mArr (← mField j "arguments") + let mut vals : Array Val := #[] + if let some recv := receiver? then + vals := vals.push (← lowerExpr recv) + for arg in args do + vals := vals.push (← lowerExpr arg) + inlineFn fnId vals j + else + failAt j s!"unsupported call kind {kind}" + +private partial def inlineFn (fnId : Nat) (args : Array Val) (at_ : Json) : M Val := do + if (← get).stack.contains fnId then failAt at_ s!"recursive call {fnId}" + let some fn := (← get).funs.find? fnId | failAt at_ s!"unresolved function {fnId}" + let saved := ← get + let savedYul := saved.yulNames + let savedFile := (← get).currentFile + let frameFile := (← get).nodeFile.find? fnId |>.getD (← get).currentFile + let frame := fnId :: (← get).stack + modify fun e => { e with stack := frame, currentFile := frameFile } + if (field? fn "virtual").bind (fun v => v.getBool?.toOption) == some true then + failAt fn "virtual dispatch is outside this slice" + let mods ← mArr (← mField fn "modifiers") + unless mods.isEmpty do failAt fn "modifiers are outside this slice" + let params ← mArr (← mField (← mField fn "parameters") "parameters") + unless params.size == args.size do + failAt at_ s!"call arity {args.size} does not match declaration {params.size}" + let rets ← mArr (← mField (← mField fn "returnParameters") "parameters") + unless rets.size == 1 do failAt fn "only single-value helpers are inlined" + let retName ← mStr (← mField rets[0]! "name") + let mut pre : Array Stmt := #[] + let mut yul := savedYul + for i in [:params.size] do + let p := params[i]! + let pname ← mStr (← mField p "name") + let pid ← mNat (← mField p "id") + let pty ← mType p + let argNode ← argumentAt at_ i + let argTy ← mType argNode + let some arg := args[i]? | failAt at_ s!"missing argument {i}" + let converted ← convert pty argTy arg at_ + let bound ← atom converted + pre := pre ++ bound.pre + modify fun e => { e with values := e.values.insert pid bound.expr } + if pname != "" then + yul := yul.insert pname bound.expr + modify fun e => { e with yulNames := yul } + noteFn fn + let body ← mField fn "body" + let stmts ← mArr (← mField body "statements") + let result ← lowerHelper stmts retName + modify fun e => + { e with stack := saved.stack, yulNames := savedYul, currentFile := savedFile, + values := saved.values, paths := saved.paths, mems := saved.mems } + pure { pre := pre ++ result.pre, expr := result.expr } + +private partial def argumentAt (call : Json) (i : Nat) : M Json := do + -- `i` counts the receiver plus explicit arguments. The receiver is not in + -- `arguments` when the call is `using for`. + let callee ← mField call "expression" + let args ← mArr (← mField call "arguments") + let hasReceiver ← match ← mKind callee with + | "MemberAccess" => + let base ← mField callee "expression" + pure !(← mType base).startsWith "type(library " + | _ => pure false + if hasReceiver then + if i == 0 then mField callee "expression" else pure args[i - 1]! + else + pure args[i]! + +private partial def convert (paramTy argTy : String) (v : Val) (at_ : Json) : M Val := do + match bitsOf paramTy, bitsOf argTy with + | some pb, some sb => + if sb ≤ pb then pure v + else failAt at_ s!"implicit narrowing from {argTy} to {paramTy}" + | _, _ => + if argTy.startsWith "int_const" || argTy == paramTy then pure v + else failAt at_ s!"unsupported implicit conversion from {argTy} to {paramTy}" + +private partial def lowerHelper (stmts : Array Json) (retName : String) : M Val := do + let mut pre : Array Stmt := #[] + let mut result : Option Val := none + for s in stmts do + if result.isSome then failAt s "statement after helper result" + match ← mKind s with + | "VariableDeclarationStatement" => + pre := pre ++ (← lowerLocal s) + | "Return" => + result := some (← lowerExpr (← mField s "expression")) + | "InlineAssembly" => + result := some (← lowerAssembly s retName) + | kind => failAt s s!"unsupported helper statement {kind}" + match result with + | some v => pure { pre := pre ++ v.pre, expr := v.expr } + | none => throwError "inlined helper did not return" + +private partial def lowerAssembly (j : Json) (retName : String) : M Val := do + let ast ← mField j "AST" + unless (← mKind ast) == "YulBlock" do failAt j "assembly is not a Yul block" + let stmts ← mArr (← mField ast "statements") + unless stmts.size == 1 do failAt j "only a single Yul assignment is supported" + let asg := stmts[0]! + unless (← mKind asg) == "YulAssignment" do failAt asg "only a Yul assignment is supported" + let vars ← mArr (← mField asg "variableNames") + unless vars.size == 1 do failAt asg "Yul assignment must have one target" + let vname ← mStr (← mField vars[0]! "name") + unless retName != "" && vname == retName do + failAt asg s!"Yul assigns {vname}, not the return name {retName}" + pure { pre := #[], expr := ← lowerYul (← mField asg "value") } + +private partial def lowerLocal (s : Json) : M (Array Stmt) := do + let decls ← mArr (← mField s "declarations") + unless decls.size == 1 do failAt s "only a single declaration is supported" + if decls[0]!.isNull then failAt s "empty declaration" + let d := decls[0]! + let name ← mStr (← mField d "name") + unless name.all (fun c => c.isAlphanum || c == '_') && name != "" do + failAt d s!"unsupported local name {name}" + let id ← mNat (← mField d "id") + let loc := optStr d "storageLocation" |>.getD "default" + let init ← mField s "initialValue" + if loc == "storage" then + match ← lowerRef init with + | .path pre path => + modify fun e => { e with paths := e.paths.insert id path } + pure pre + | _ => failAt s "storage local is not a resolved read path" + else + let v ← lowerExpr init + let binding ← freshFor name + let expr := Expr.localVar binding + modify fun e => + { e with values := e.values.insert id expr, yulNames := e.yulNames.insert name expr } + pure (v.pre.push (.letVar binding v.expr)) + +end + +private def structMemberList (st : Json) : M (Array (String × String)) := do + let ms ← mArr (← mField st "members") + let mut out : Array (String × String) := #[] + for m in ms do + out := out.push (← mStr (← mField m "name"), ← mType m) + pure out + +private def bindRoot (fn : Json) : M (Array SrcParam) := do + noteFn fn + let params ← mArr (← mField (← mField fn "parameters") "parameters") + let mut out : Array SrcParam := #[] + for p in params do + let name ← mStr (← mField p "name") + let id ← mNat (← mField p "id") + let ty ← mType p + let loc := optStr p "storageLocation" |>.getD "default" + out := out.push { name, id } + modify fun e => { e with bound := name :: e.bound } + if ty.startsWith "struct " && (loc == "memory" || loc == "calldata") then + let typeName ← mField p "typeName" + let sid ← refInt typeName + if sid < 0 then failAt p "builtin struct" + let some decl := (← get).structs.find? sid.toNat | failAt p s!"unresolved struct {ty}" + let members ← structMemberList decl + let sname ← mStr (← mField decl "name") + modify fun e => + let bound : MemParam := ⟨name, sname, members⟩ + { e with mems := e.mems.insert id bound } + else if ty.startsWith "struct " then + failAt p s!"unsupported location {loc} for {ty}" + else + let some pty := paramType ty | failAt p s!"unsupported parameter type {ty}" + modify fun e => + let values := e.values.insert id (.param name) + let scalarTy := e.scalarTy.insert id pty + { e with values, scalarTy } + pure out + +private def lowerRoot (fn : Json) : M (Array Stmt × Array SrcParam) := do + let rootId ← mNat (← mField fn "id") + modify fun e => { e with stack := [rootId] } + let srcParams ← bindRoot fn + if (field? fn "virtual").bind (fun v => v.getBool?.toOption) == some true then + failAt fn "virtual dispatch is outside this slice" + let mods ← mArr (← mField fn "modifiers") + unless mods.isEmpty do failAt fn "modifiers are outside this slice" + let some _ := field? fn "body" | failAt fn "function has no body" + let stmts ← mArr (← mField (← mField fn "body") "statements") + let mut out : Array Stmt := #[] + let mut returned := false + for s in stmts do + if returned then failAt s "statement after root return" + match ← mKind s with + | "VariableDeclarationStatement" => + out := out ++ (← lowerLocal s) + | "Return" => + returned := true + let expr ← mField s "expression" + if (← mKind expr) == "TupleExpression" then + if ← mBool (← mField expr "isInlineArray") then + failAt expr "inline arrays are outside this slice" + let cs ← mArr (← mField expr "components") + let mut pres : Array Stmt := #[] + let mut exprs : Array Expr := #[] + for c in cs do + if c.isNull then failAt expr "empty return component" + let v ← atom (← lowerExpr c) + pres := pres ++ v.pre + exprs := exprs.push v.expr + out := out ++ pres |>.push (.returnValues exprs.toList) + else + let v ← atom (← lowerExpr expr) + out := out ++ v.pre |>.push (.returnValues [v.expr]) + | kind => failAt s s!"unsupported statement {kind}" + unless returned do failAt fn "an explicit root return is required" + pure (out, srcParams) + +private partial def index (file : String) (contract? : Option String) (j : Json) : M Unit := do + match j with + | .obj o => + if let some name := optStr j "name" then + modify fun e => { e with sourceNames := name :: e.sourceNames } + if (field? j "nodeType").isSome then + if let some idj := field? j "id" then + let id ← mNat idj + modify fun e => { e with nodeFile := e.nodeFile.insert id file } + match ← mKind j with + | "FunctionDefinition" => + modify fun e => + let funs := e.funs.insert id j + let funContract := e.funContract.insert id (contract?.getD "") + { e with funs, funContract } + | "StructDefinition" => + modify fun e => { e with structs := e.structs.insert id j } + | "VariableDeclaration" => + let isState := match field? j "stateVariable" with + | some b => match b.getBool? with | .ok v => v | _ => false + | none => false + if isState then + modify fun e => { e with stateVars := e.stateVars.insert id j } + | _ => pure () + let next ← do + if (field? j "nodeType") == some (Json.str "ContractDefinition") then + let n ← mStr (← mField j "name") + pure (some n) + else + pure contract? + let children := o.foldl (fun acc _ v => v :: acc) [] + for child in children do + index file next child + | .arr xs => + for child in xs do + index file contract? child + | _ => pure () + +private def normalizePath (path : String) : Except String String := Id.run do + let mut out : Array String := #[] + for part in path.splitOn "/" do + if part == "" || part == "." then + pure () + else if part == ".." then + if out.isEmpty then return .error s!"path escapes the project: {path}" + out := out.pop + else + out := out.push part + return .ok (String.intercalate "/" out.toList) + +private def resolveSpec (origin spec : String) (remaps : Array (String × String)) : Except String String := do + if spec.startsWith "." then + let parent := String.intercalate "/" (origin.splitOn "/").dropLast + normalizePath (parent ++ "/" ++ spec) + else + let mut best : Option (String × String) := none + for pair in remaps do + if spec.startsWith pair.1 then + best := match best with + | none => some pair + | some prev => if pair.1.length > prev.1.length then some pair else some prev + match best with + | some (pre, tgt) => normalizePath (tgt ++ spec.drop pre.length) + | none => normalizePath spec + +private def importSpecs (text : String) : Array String := Id.run do + let mut out : Array String := #[] + for line in text.splitOn "\n" do + let t := line.trimAscii.toString + if t.startsWith "import" then + let parts := t.splitOn "\"" + if parts.length ≥ 2 then + out := out.push parts[1]! + pure out + +private def readRemappings (root : System.FilePath) : IO (Array (String × String)) := do + let path := root / "remappings.txt" + if !(← path.pathExists) then return #[] + let text ← IO.FS.readFile path + let mut out : Array (String × String) := #[] + for line in text.splitOn "\n" do + let t := line.trimAscii.toString + if t == "" || t.startsWith "#" || t.startsWith "//" then continue + let parts := t.splitOn "=" + if parts.length == 2 then + out := out.push (parts[0]!, parts[1]!) + pure out + +private partial def collectSources + (root : System.FilePath) (logical : String) + (remaps : Array (String × String)) + (acc : RBMap String String compare) : MetaM (RBMap String String compare) := do + if acc.contains logical then return acc + let path := root / logical + unless ← path.pathExists do throwError "missing Solidity source {logical}" + let text ← IO.FS.readFile path + let mut acc := acc.insert logical text + for spec in importSpecs text do + match resolveSpec logical spec remaps with + | .error e => throwError "{logical}: {e}" + | .ok next => acc ← collectSources root next remaps acc + pure acc + +private def verifyCompiler (compiler : System.FilePath) : MetaM String := do + let output ← + if System.Platform.isOSX then + IO.Process.output { cmd := "/usr/bin/shasum", args := #["-a", "256", compiler.toString] } + else + IO.Process.output { cmd := "/usr/bin/sha256sum", args := #[compiler.toString] } + unless output.exitCode == 0 && officialSolcSha256s.contains (output.stdout.take 64).toString do + throwError "compiler checksum mismatch" + return (output.stdout.take 64).toString + +/-- Verity itself stores the importer next to its lakefile. A downstream package +stores that checkout at `.lake/packages/verity`. The digest must hash those +sources in either layout. -/ +private def importerSourceRoot (pkgRoot : System.FilePath) : MetaM System.FilePath := do + if ← (pkgRoot / "Compiler/SolidityImport/Import.lean").pathExists then + return pkgRoot + let dep := pkgRoot / ".lake/packages/verity" + if ← (dep / "Compiler/SolidityImport/Import.lean").pathExists then + return dep + throwError "importer source missing: {pkgRoot / "Compiler/SolidityImport/Import.lean"}" + +private def moduleText (pkgRoot : System.FilePath) (rel : String) : MetaM String := do + let path := pkgRoot / rel + unless ← path.pathExists do throwError "importer source missing: {path}" + IO.FS.readFile path + +private def FnRec.toImported (f : FnRec) : ImportedFunction := + { contract := f.contract, name := f.name, declId := f.declId, paramTypes := f.paramTypes.toList } + +/-- Solidity spelling of a solc `typeString`: `struct Market memory` → `Market`. -/ +private def sourceTypeName (typeString : String) : String := Id.run do + let mut t := typeString + for pre in ["struct ", "contract ", "enum "] do + if t.startsWith pre then t := (t.drop pre.length).toString + for suf in [" storage pointer", " storage ref", " memory", " calldata", " storage"] do + if t.endsWith suf then t := (t.dropEnd suf.length).toString + return t + +/-- A written type matches its Solidity spelling, optionally qualified (`I.Market`). -/ +private def typeMatches (written typeString : String) : Bool := + let t := sourceTypeName typeString + t == written || t.endsWith ("." ++ written) || written.endsWith ("." ++ t) + +/-- Select the function; also return its solc parameter type strings. -/ +private def selectFunction (contract functionName : String) (written : Array String) : + M (Json × Array String) := do + let env ← get + let mut hits : Array (Json × Array String) := #[] + let mut described : Array String := #[] + for (id, fn) in env.funs do + let name ← mStr (← mField fn "name") + let owner := env.funContract.find? id |>.getD "" + let params ← mArr (← mField (← mField fn "parameters") "parameters") + let mut tys : Array String := #[] + for p in params do + tys := tys.push (← mType p) + if owner == contract && name == functionName then + described := described.push + s!"{functionName}({String.intercalate ", " (tys.map sourceTypeName).toList})" + if tys.size == written.size && (tys.zip written).all (fun (t, w) => typeMatches w t) then + hits := hits.push (fn, tys) + let sig := s!"{contract}.{functionName}({String.intercalate ", " written.toList})" + if hits.size == 0 then + throwError "no function {sig}; candidates: {described}" + if hits.size > 1 then + throwError "ambiguous function {sig}; qualify the parameter types" + let (fn, tys) := hits[0]! + let implemented ← match field? fn "implemented" with + | some b => mBool b + | none => pure true + unless implemented do throwError "function is not implemented" + pure (fn, tys) + +private def importSlice + (pkgRoot projectRoot : System.FilePath) (entry contract : String) + (roots : Array (String × Array String)) (profile : Profile) : + MetaM (CompilationModel × ImportReport) := do + let compiler := pkgRoot / ".lake/solidity-import/solc-0.8.34" + let solcSha ← verifyCompiler compiler + let versionOut ← IO.Process.output { cmd := compiler.toString, args := #["--version"] } + unless versionOut.exitCode == 0 && + acceptedSolcBanners.contains versionOut.stdout.trimAscii.toString do + throwError "compiler version mismatch" + unless (← verifyCompiler compiler) == solcSha do throwError "compiler changed during import" + let remaps ← readRemappings projectRoot + let sources ← collectSources projectRoot entry remaps RBMap.empty + let mut fileBytes : RBMap String ByteArray compare := RBMap.empty + let mut sourceObj : Array (String × Json) := #[] + for (logical, text) in sources do + fileBytes := fileBytes.insert logical (text.toUTF8) + sourceObj := sourceObj.push (logical, Json.mkObj [("content", Json.str text)]) + let settings := Json.mkObj [ + ("evmVersion", Json.str profile.evmVersion), + ("metadata", Json.mkObj [("bytecodeHash", Json.str profile.bytecodeHash)]), + ("optimizer", Json.mkObj [("enabled", Json.bool profile.optimizerRuns.isSome), + ("runs", profile.optimizerRuns.getD 200)]), + ("outputSelection", Json.mkObj [("*", Json.mkObj [ + ("", Json.arr #[Json.str "ast"]), + ("*", Json.arr #[Json.str "storageLayout"])])]), + ("remappings", Json.arr (remaps.map fun p => Json.str s!"{p.1}={p.2}")), + ("viaIR", Json.bool profile.viaIR)] + let input := Json.mkObj [ + ("language", Json.str "Solidity"), + ("settings", settings), + ("sources", Json.mkObj sourceObj.toList)] + let output ← IO.Process.output + { cmd := compiler.toString, args := #["--standard-json", "--no-import-callback"] } + (some input.compress) + unless output.exitCode == 0 do throwError "solc failed: {output.stderr}" + unless (← verifyCompiler compiler) == solcSha do throwError "compiler changed during import" + let parsed ← match Json.parse output.stdout with + | .ok v => pure v + | .error e => throwError "solc output is not JSON: {e}" + if let some errors := field? parsed "errors" then + for err in ← arr errors do + let severity := optStr err "severity" |>.getD "" + if severity == "error" then + throwError "solc: {optStr err "formattedMessage" |>.getD "error"}" + let mut env := Env.init + env := { env with files := fileBytes } + let sourcesOut ← field parsed "sources" + for (logical, _) in sources do + let unit ← field sourcesOut logical + let ast ← field unit "ast" + ((), env) ← (index logical none ast).run env + let contracts ← field parsed "contracts" + let entryContracts ← field contracts entry + let chosen ← field entryContracts contract + let layout ← field chosen "storageLayout" + let types ← field layout "types" + let items ← arr (← field layout "storage") + env := { env with layoutTypes := types } + for item in items do + let label ← str (← field item "label") + env := { env with layoutItems := env.layoutItems.insert label item } + let mut specs : Array FunctionSpec := #[] + let mut projections : Array ParamProjection := #[] + let mut signatures : Array Json := #[] + let mut rootIds : Array Nat := #[] + for (functionName, written) in roots do + let (fn, paramTys) ← (selectFunction contract functionName written).run' env + let rootId ← flexNat (← field fn "id") + if rootIds.contains rootId then throwError "function {contract}.{functionName} is imported twice" + rootIds := rootIds.push rootId + signatures := signatures.push (Json.mkObj + [("function", Json.str functionName), ("parameterTypes", Json.arr (paramTys.map Json.str))]) + -- Each root is lowered on its own: bindings, generated names and projections + -- do not leak between functions. Field layouts and the closure are shared. + env := { env with currentFile := entry, next := 0, bound := [], values := RBMap.empty, paths := RBMap.empty, + mems := RBMap.empty, scalarTy := RBMap.empty, yulNames := RBMap.empty, + projections := #[] } + let ((body, srcParams), env2) ← (lowerRoot fn).run env + env := env2 + let mut modelParams : Array Param := #[] + let mut modelParamNames : Array String := #[] + for p in srcParams do + if env.mems.contains p.id then + let some mem := env.mems.find? p.id | throwError "missing memory parameter {p.name}" + let mut seen := false + for (member, ty) in mem.members do + if let some proj := env.projections.find? (fun q => q.parameter == p.name && q.member == member) then + let some pty := paramType ty + | (failAt fn s!"unsupported projected type {ty}").run' env + if modelParamNames.contains proj.modelParam then + (failAt fn s!"projected parameter name collision: {proj.modelParam}").run' env + modelParamNames := modelParamNames.push proj.modelParam + modelParams := modelParams.push { name := proj.modelParam, ty := pty } + seen := true + unless seen do (failAt fn s!"struct parameter {p.name} was not read").run' env + else + let some pty := env.scalarTy.find? p.id | throwError "missing parameter type" + if modelParamNames.contains p.name then (failAt fn s!"projected parameter name collision: {p.name}").run' env + modelParamNames := modelParamNames.push p.name + modelParams := modelParams.push { name := p.name, ty := pty } + let returns ← (do + let rets ← mArr (← mField (← mField fn "returnParameters") "parameters") + let mut out : Array ParamType := #[] + for r in rets do + let ty ← mType r + let some pty := paramType ty | failAt r s!"unsupported return type {ty}" + out := out.push pty + pure out) |>.run' env + let mutability ← str (← field fn "stateMutability") + let isView := mutability == "view" || mutability == "pure" + specs := specs.push + { name := functionName, params := modelParams.toList, returnType := none, + returns := returns.toList, isView, body := body.toList } + for proj in env.projections do + let ignored := + match env.mems.toList.find? (fun pair => pair.2.param == proj.parameter) with + | some (_, mem) => mem.members.filterMap fun (pair : String × String) => + if env.projections.any (fun q => q.parameter == proj.parameter && q.member == pair.1) + then none else some pair.1 + | none => #[] + projections := projections.push + { function := functionName, parameter := proj.parameter, member := proj.member, + structName := proj.structName, headWord := proj.headWord, modelParam := proj.modelParam, + ignoredMembers := ignored.toList } + let mut fields : Array (Nat × Field) := #[] + for name in env.referenced do + let some info := env.fieldsByName.find? name | throwError "missing field {name}" + fields := fields.push (info.slot, info.field) + let sortedFields := (fields.qsort (fun a b => a.1 < b.1)).map (·.2) + let model : CompilationModel := + { name := contract, constructor := none, fields := sortedFields.toList, functions := specs.toList } + let mut excluded : Array FnRec := #[] + for (id, decl) in env.funs do + unless env.included.any (·.declId == id) do + let name ← str (← field decl "name") + let params ← arr (← field (← field decl "parameters") "parameters") + let mut tys : Array String := #[] + for p in params do + tys := tys.push (← typeString p) + excluded := excluded.push { + contract := env.funContract.find? id |>.getD "", + name, declId := id, paramTypes := tys } + let sortedExcluded := excluded.qsort (fun a b => a.declId < b.declId) + let mut opaqueMembers : Array OpaqueMember := #[] + for o in env.opaqueMembers do + if env.referenced.contains o.field then + opaqueMembers := opaqueMembers.push + { field := o.field, name := o.name, solcType := o.solcType, + wordOffset := o.wordOffset, byteOffset := o.byteOffset } + let sourceRoot ← importerSourceRoot pkgRoot + let importer ← moduleText sourceRoot "Compiler/SolidityImport/Import.lean" + let coverage ← moduleText sourceRoot "Compiler/SolidityImport/Coverage.lean" + let reportSrc ← moduleText sourceRoot "Compiler/SolidityImport/Report.lean" + let quoteSrc ← moduleText sourceRoot "Compiler/SolidityImport/Quote.lean" + let profileSrc ← moduleText sourceRoot "Compiler/SolidityImport/Profile.lean" + -- JSON framing prevents distinct file/signature lists from sharing a + -- concatenation merely because their text contains separator newlines. + let digestInput := Json.mkObj [ + ("importerVersion", Json.str importerVersion), + ("solcVersion", Json.str solcLongVersion), + ("contract", Json.str contract), + ("functions", Json.arr signatures), + ("solcInput", input), + ("importerSources", Json.mkObj [ + ("Import.lean", Json.str importer), ("Coverage.lean", Json.str coverage), + ("Report.lean", Json.str reportSrc), ("Quote.lean", Json.str quoteSrc), + ("Profile.lean", Json.str profileSrc)])] + let digest := sha256Hex digestInput.compress.toUTF8 + let report : ImportReport := + { importerVersion, solcLongVersion, solcSha256 := solcSha, settingsJson := settings.compress, + sourceDigest := digest, contract, roots := (roots.map (·.1)).toList, + functions := specs.toList.map fun f => + { function := f.name, denoteCovered := stmtListCovered f.body, + compilerProof := .unavailable noCompilerProofReason }, + includedFunctions := (env.included.map FnRec.toImported).toList, + excludedFunctions := (sortedExcluded.map FnRec.toImported).toList, + projections := projections.toList, storageFields := env.referenced.toList, + storageKeys := env.referenced.toList.map fun name => (name, mappingKeyNames env name), + opaqueMembers := opaqueMembers.toList, observesPanicPayload := false } + pure (model, report) + +/-- `solidity_profile osaka466 where evmVersion := "osaka" …` defines a named +`Profile`, reusable by several imports and printable with `#print`. -/ +syntax (name := solidityProfileCmd) "solidity_profile " ident " where" + sepByIndentSemicolon(Lean.Parser.Term.structInstField) : command + +macro_rules + | `(solidity_profile $id where $fields;*) => do + let fields : Syntax.TSepArray `Lean.Parser.Term.structInstField ", " := fields.getElems + `(def $id : Compiler.CompilationModel.SolidityImport.Profile := { $fields:structInstField,* }) + +/-- `function name(T₁, …, Tₙ)`: a function to import, with its Solidity parameter types. -/ +syntax solidityRoot := &"function" ident "(" ident,* ")" + +/-- Import Solidity functions as a `CompilationModel`; see the module docstring. -/ +syntax (name := solidityImportCmd) + "solidity_import " ident &"from" str &"entry" str " using " term:max + &"contract" ident (ppLine solidityRoot)+ : command + +private unsafe def evalProfileUnsafe (stx : Term) : TermElabM Profile := do + let e ← Term.elabTermEnsuringType stx (mkConst ``Profile) + Term.synthesizeSyntheticMVarsNoPostponing + let e ← instantiateMVars e + if e.hasMVar then throwError "the solc profile is not fully determined" + Meta.evalExpr Profile (mkConst ``Profile) e + +@[implemented_by evalProfileUnsafe] +private opaque evalProfile (stx : Term) : TermElabM Profile + +private unsafe def evalModelUnsafe (n : Name) : TermElabM CompilationModel := + evalConstCheck CompilationModel ``CompilationModel n + +@[implemented_by evalModelUnsafe] +private opaque evalModel (n : Name) : TermElabM CompilationModel + +private unsafe def evalReportUnsafe (n : Name) : TermElabM ImportReport := + evalConstCheck ImportReport ``ImportReport n + +@[implemented_by evalReportUnsafe] +private opaque evalReport (n : Name) : TermElabM ImportReport + +/-- The directory holding the importing package's `lakefile.lean`. -/ +private def packageRoot : CommandElabM System.FilePath := do + let mut pkg := (← IO.FS.realPath (← getFileName)).parent.getD "." + while !(← (pkg / "lakefile.lean").pathExists) do + let some parent := pkg.parent | throwError "package root not found" + if parent == pkg then throwError "package root not found" + pkg := parent + return pkg + +/-! ## Typed accessors + +For readable specifications, the import also defines, under its namespace, one +typed function per imported root (`midnight.updatePositionView`) and one typed +reader per storage struct member (`midnight.position.credit`). They only wrap +`runFunction` and `readMember` on the imported model. -/ + +private def keyTypeTerm : MappingKeyType → CommandElabM Term + | .address => `(Verity.Core.Address) + | .bytes32 => `(Verity.Core.BytesN 32) + | .uint256 => `(Verity.Core.Uint256) + +/-- Lean type of a scalar parameter or return value, when it has one. -/ +private def paramTypeTerm? : ParamType → Option (CommandElabM Term) + | .uint256 => some `(Verity.Core.Uint256) + | .uintN bits => some `(Verity.Core.UIntN $(quote bits)) + | .uint8 => some `(Verity.Core.UIntN 8) + | .uint16 => some `(Verity.Core.UIntN 16) + | .address => some `(Verity.Core.Address) + | .bytes32 => some `(Verity.Core.BytesN 32) + | .bool => some `(Bool) + | _ => none + +private def docComment (text : String) : TSyntax ``Lean.Parser.Command.docComment := + ⟨mkNode ``Lean.Parser.Command.docComment #[mkAtom "/--", mkAtom (text ++ "-/")]⟩ + +/-- A binder name for `base` that no Solidity parameter uses. -/ +private def freeBinder (taken : List String) (base : String) : Ident := Id.run do + let mut name := base + while taken.contains name do name := name ++ "'" + pure (mkIdent (Name.mkSimple name)) + +private def elabStorageAccessors (ns : Name) (model : CompilationModel) (report : ImportReport) : + CommandElabM (List Name) := do + let mut names := [] + for field in model.fields do + let (keys, members) ← match field.ty with + | .mappingStruct k members => pure ([k], members) + | .mappingStruct2 k1 k2 members => pure ([k1, k2], members) + | _ => continue + let solNames := (report.storageKeys.find? (·.1 == field.name)).map (·.2) |>.getD [] + let keyIdents := (List.range keys.length).map fun i => + let solName := solNames.getD i "" + let usable := solName != "" && !["oracle", "world"].contains solName && + (solNames.filter (· == solName)).length == 1 + mkIdent (Name.mkSimple (if usable then solName else s!"key{i + 1}")) + let mut binders : Array (TSyntax ``Lean.Parser.Term.bracketedBinder) := #[] + for (k, ident) in keys.zip keyIdents do + binders := binders.push (← `(bracketedBinder| ($ident : $(← keyTypeTerm k)))) + let keyWords ← keyIdents.toArray.mapM fun ident => `(($ident).val) + let path := String.join (keyIdents.map fun _ => "[..]") + for member in members do + let name := ns ++ Name.mkSimple field.name ++ Name.mkSimple member.name + let valName := ns ++ Name.mkSimple field.name ++ Name.mkSimple (member.name ++ "_val") + let read ← `(Compiler.CompilationModel.SolidityImport.readMember oracle $(mkIdent (`_root_ ++ ns ++ `model)) + world $(quote field.name) [$keyWords,*] $(quote member.name)) + let doc := docComment s!"`{field.name}{path}.{member.name}`, read with the imported storage layout. " + match member.packed with + | some packed => + elabCommand (← `($doc:docComment def $(mkIdent (`_root_ ++ name)) + (oracle : Compiler.CompilationModel.Denote.DenoteOracle) (world : Verity.ContractState) + $binders* : Verity.Core.UIntN $(quote packed.width) := + Verity.Core.UIntN.ofNat $(quote packed.width) $read)) + elabCommand (← `(@[simp] theorem $(mkIdent (`_root_ ++ valName)) + (oracle : Compiler.CompilationModel.Denote.DenoteOracle) (world : Verity.ContractState) + $binders* : ($(mkIdent (`_root_ ++ name)) oracle world $keyIdents.toArray*).val = $read := + Compiler.CompilationModel.SolidityImport.val_uintN_readMember _ _ _ _ _ _ (by decide))) + | none => + elabCommand (← `($doc:docComment def $(mkIdent (`_root_ ++ name)) + (oracle : Compiler.CompilationModel.Denote.DenoteOracle) (world : Verity.ContractState) + $binders* : Verity.Core.Uint256 := + Verity.Core.Uint256.ofNat $read)) + elabCommand (← `(@[simp] theorem $(mkIdent (`_root_ ++ valName)) + (oracle : Compiler.CompilationModel.Denote.DenoteOracle) (world : Verity.ContractState) + $binders* : ($(mkIdent (`_root_ ++ name)) oracle world $keyIdents.toArray*).val = $read := + Compiler.CompilationModel.SolidityImport.val_uint256_readMember _ _ _ _ _ _ (by decide))) + names := name :: valName :: names + pure names + +private def elabFunctionCalls (ns : Name) (model : CompilationModel) : CommandElabM (List Name) := do + let mut names := [] + for fn in model.functions do + let taken := fn.params.map (·.name) + let some paramTys := fn.params.mapM (paramTypeTerm? ·.ty) | continue + let some returnTys := fn.returns.mapM paramTypeTerm? | continue + let oracle := freeBinder taken "oracle" + let world := freeBinder taken "world" + let paramIdents := fn.params.map fun p => mkIdent (Name.mkSimple p.name) + let mut binders : Array (TSyntax ``Lean.Parser.Term.bracketedBinder) := #[] + for (ident, ty) in paramIdents.zip paramTys do + binders := binders.push (← `(bracketedBinder| ($ident : $(← ty)))) + let args ← (fn.params.zip paramIdents).toArray.mapM fun (p, ident) => + `(($(quote p.name), Compiler.CompilationModel.SolidityImport.Word.toWord $ident)) + let returnTys ← returnTys.mapM id + let resultTy ← match returnTys.reverse with + | [] => `(Unit) + | last :: rest => rest.foldlM (fun acc ty => `($ty × $acc)) last + let words := (List.range fn.returns.length).toArray.map fun i => mkIdent (Name.mkSimple s!"r{i}") + let decoded ← words.mapM fun w => `(Compiler.CompilationModel.SolidityImport.Word.ofWord $w) + let result ← match decoded.toList with + | [] => `(()) + | [x] => pure x + | x :: xs => `(($x, $(xs.toArray),*)) + let name := ns ++ Name.mkSimple fn.name + let doc := docComment s!"Call `{fn.name}` on the imported model in `{world.getId}`; `none` means it reverts. " + elabCommand (← `($doc:docComment def $(mkIdent (`_root_ ++ name)) + ($oracle : Compiler.CompilationModel.Denote.DenoteOracle) ($world : Verity.ContractState) + $binders* : Option $resultTy := + match Compiler.CompilationModel.SolidityImport.runFunction $oracle $(mkIdent (`_root_ ++ ns ++ `model)) + $(quote fn.name) $world [$args,*] with + | some [$words,*] => some $result + | _ => none)) + names := name :: names + pure names + +@[command_elab solidityImportCmd] +def elabSolidityImport : CommandElab := fun stx => do + let `(solidity_import $alias from $root entry $entry using $prof contract $contract + $roots:solidityRoot*) := stx | throwUnsupportedSyntax + let saved ← getEnv + try + if debug.skipKernelTC.get (← getOptions) then + throwError "kernel checking must be enabled" + let profile ← liftTermElabM <| evalProfile prof + unless profile.solc == solcLongVersion do + throwError "this importer is pinned to solc {solcLongVersion}, not {profile.solc}" + let roots ← roots.mapM fun root => do + let `(solidityRoot| function $fn ( $tys,* )) := root | throwUnsupportedSyntax + pure (fn.getId.toString, tys.getElems.map (·.getId.toString (escape := false))) + let pkg ← packageRoot + let project := if root.getString.startsWith "/" then + System.FilePath.mk root.getString else pkg / root.getString + let (model, report) ← liftTermElabM <| + importSlice pkg project entry.getString contract.getId.toString roots profile + let ns := (← getCurrNamespace) ++ alias.getId + let name (suffix : Name) := mkIdent (`_root_ ++ ns ++ suffix) + elabCommand (← `(def $(name `model) : Compiler.CompilationModel.CompilationModel := + $(← quoteModel model))) + elabCommand (← `(def $(name `report) : Compiler.CompilationModel.SolidityImport.ImportReport := + $(← quoteReport report))) + elabCommand (← `(def $(name `sourceDigest) : String := $(quote report.sourceDigest))) + elabCommand (← `(theorem $(name `covered) : + Compiler.CompilationModel.SolidityImport.modelImportCovered $(name `model) = true := by decide)) + for fn in model.functions do + if [`model, `report, `sourceDigest, `covered].contains (Name.mkSimple fn.name) then + throwError "imported function {fn.name} collides with the generated {ns ++ Name.mkSimple fn.name}" + let generated := (← elabFunctionCalls ns model) ++ (← elabStorageAccessors ns model report) + if let some dup := generated.find? (fun n => (generated.filter (· == n)).length > 1) then + throwError "generated accessor {dup} is defined twice" + -- The quoted definitions must denote exactly the values the importer built. + unless (← get).messages.hasErrors do + let back ← liftTermElabM <| evalModel (ns ++ `model) + unless toString (repr back) == toString (repr model) do + throwError "internal: the elaborated model differs from the imported value" + let backReport ← liftTermElabM <| evalReport (ns ++ `report) + unless backReport == report do + throwError "internal: the elaborated report differs from the imported value" + if (← get).messages.hasErrors then + throwError "Solidity import failed to check" + catch e => + setEnv saved + throw e + +end Compiler.CompilationModel.SolidityImport diff --git a/Compiler/SolidityImport/Profile.lean b/Compiler/SolidityImport/Profile.lean new file mode 100644 index 0000000000..77fe2c086f --- /dev/null +++ b/Compiler/SolidityImport/Profile.lean @@ -0,0 +1,27 @@ +/-! +Compiler settings for a Solidity import. + +A profile is an ordinary value, so it can be named, reused and `#print`ed: + +```lean +def build : Profile := + { evmVersion := "osaka", viaIR := true, optimizerRuns := some 466, bytecodeHash := "none" } +``` + +The settings are passed to solc and are part of the import digest. They should +match the settings the audited bytecode is built with. +-/ +namespace Compiler.CompilationModel.SolidityImport + +structure Profile where + /-- Exact solc release. The importer accepts only its pinned release. -/ + solc : String := "0.8.34+commit.80d5c536" + evmVersion : String + viaIR : Bool + /-- Optimizer runs; `none` disables the optimizer. -/ + optimizerRuns : Option Nat + /-- solc `metadata.bytecodeHash`, e.g. `"none"` or `"ipfs"`. -/ + bytecodeHash : String + deriving Repr, BEq, Inhabited + +end Compiler.CompilationModel.SolidityImport diff --git a/Compiler/SolidityImport/Proofs.lean b/Compiler/SolidityImport/Proofs.lean new file mode 100644 index 0000000000..3ccca77257 --- /dev/null +++ b/Compiler/SolidityImport/Proofs.lean @@ -0,0 +1,218 @@ +import Mathlib.Tactic +import Compiler.SolidityImport.Access + +/-! +Lemmas for proofs about an imported model's `Denote` execution: 256-bit word +arithmetic, environment bindings, and splitting a body into straight-line parts. +-/ + +open Compiler.CompilationModel Compiler.CompilationModel.Denote +open Verity.Core +namespace Compiler.CompilationModel.SolidityImport + +/-- Split a body just after its top-level `let name := ...`. Since imported +Solidity locals keep their name, this cuts a body at a Solidity statement. -/ +def splitAfter (name : String) : List Stmt → List Stmt × List Stmt + | [] => ([], []) + | stmt :: rest => + match stmt with + | .letVar n _ => if n = name then ([stmt], rest) else + (stmt :: (splitAfter name rest).1, (splitAfter name rest).2) + | _ => (stmt :: (splitAfter name rest).1, (splitAfter name rest).2) + +/-! ## Words -/ + +def max128 : Nat := 2^128 - 1 + +theorem max128_lt : max128 < Uint256.modulus := by decide + +theorem word_of_small {a : Nat} (h : a ≤ max128) : (Uint256.ofNat a).val = a := + Nat.mod_eq_of_lt (lt_of_le_of_lt h max128_lt) + +@[simp] theorem normalize_normalize (n : Nat) : wordNormalize (wordNormalize n) = wordNormalize n := + Nat.mod_mod _ _ + +@[simp] theorem normalize_max : wordNormalize max128 = max128 := word_of_small (Nat.le_refl _) + +theorem sub_word {a b : Nat} (ha : a < Uint256.modulus) (hb : b ≤ a) : + (Uint256.ofNat a - Uint256.ofNat b).val = a - b := by + change (Uint256.sub _ _).val = _ + simp [Uint256.sub, Uint256.ofNat, Nat.mod_eq_of_lt ha, + Nat.mod_eq_of_lt (lt_of_le_of_lt hb ha), hb, + Nat.mod_eq_of_lt (lt_of_le_of_lt (Nat.sub_le a b) ha)] + +theorem mul_word128 {a b : Nat} (ha : a ≤ max128) (hb : b ≤ max128) : + (Uint256.ofNat a * Uint256.ofNat b).val = a * b := by + have hp : a * b < Uint256.modulus := lt_of_le_of_lt (Nat.mul_le_mul ha hb) (by decide) + change (Uint256.mul _ _).val = _ + simp [Uint256.mul, Uint256.ofNat, Nat.mod_eq_of_lt hp, + Nat.mod_eq_of_lt (lt_of_le_of_lt ha max128_lt), + Nat.mod_eq_of_lt (lt_of_le_of_lt hb max128_lt)] + +theorem div_word {a b : Nat} (ha : a < Uint256.modulus) (hb : b < Uint256.modulus) : + (Uint256.ofNat a / Uint256.ofNat b).val = a / b := by + change (Uint256.div _ _).val = _ + by_cases h : b = 0 + · subst b; simp [Uint256.div, Uint256.ofNat] + · simp [Uint256.div, Uint256.ofNat, Nat.mod_eq_of_lt ha, Nat.mod_eq_of_lt hb, h, + Nat.mod_eq_of_lt (lt_of_le_of_lt (Nat.div_le_self a b) ha)] + +theorem mask_le (a : Nat) : (Uint256.and (Uint256.ofNat a) (Uint256.ofNat max128)).val ≤ a := by + exact le_trans (Nat.mod_le _ _) (le_trans Nat.and_le_left (Nat.mod_le _ _)) + +theorem mask_bounded (a : Nat) : (Uint256.and (Uint256.ofNat a) (Uint256.ofNat max128)).val ≤ max128 := by + exact le_trans (Nat.mod_le _ _) (le_trans Nat.and_le_right (Nat.mod_le _ _)) + +theorem mask_eq {a : Nat} (ha : a ≤ max128) : + (Uint256.and (Uint256.ofNat a) (Uint256.ofNat max128)).val = a := by + have h : a < 2^128 := by unfold max128 at ha; omega + simp only [Uint256.and, Uint256.ofNat, Nat.mod_eq_of_lt (lt_of_le_of_lt ha max128_lt), + Nat.mod_eq_of_lt max128_lt] + rw [Nat.land_eq, max128, Nat.and_two_pow_sub_one_eq_mod, Nat.mod_eq_of_lt h, + Nat.mod_eq_of_lt (lt_of_le_of_lt ha max128_lt)] + +theorem ofNat_val (u : Uint256) : Uint256.ofNat u.val = u := by + apply Uint256.ext + exact Nat.mod_eq_of_lt u.isLt + +/-! ## Execution -/ + +@[simp] theorem lookup_bind_same (env : Env) (name : String) (v : Nat) : + lookupValue (bindValue env name v) name = v := by + simp [lookupValue, bindValue] + +@[simp] theorem lookup_bind_other (env : Env) (name key : String) (v : Nat) + (h : name ≠ key) : lookupValue (bindValue env name v) key = lookupValue env key := by + induction env with + | nil => simp [lookupValue, bindValue, h] + | cons e es ih => + rcases e with ⟨n, x⟩ + by_cases hn : n = name + · subst n + simpa [lookupValue, bindValue, h] using ih + · by_cases hk : n = key + · subst n; simp [lookupValue, bindValue, h, Ne.symm h] + · simpa [lookupValue, bindValue, h, hn, hk] using ih + +mutual +/-- A prefix can only continue or revert, and cannot assign a protected name. -/ + def prefixStmt (keys : List String) : Stmt → Bool + | .letVar name _ | .assignVar name _ => !keys.contains name + | .ite _ yes no => prefixList keys yes && prefixList keys no + | .panic _ => true + | _ => false + def prefixList (keys : List String) : List Stmt → Bool + | [] => true + | s :: ss => prefixStmt keys s && prefixList keys ss +end + +def Frame (keys : List String) (s : DenoteState) : StmtOutcome → Prop + | .continue t => t.world = s.world ∧ ∀ k ∈ keys, lookupValue t.bindings k = lookupValue s.bindings k + | .revert => True + | _ => False + +mutual + theorem stmt_frame (o : DenoteOracle) (fs : List Field) (s : DenoteState) + (stmt : Stmt) (keys : List String) (h : prefixStmt keys stmt = true) : + Frame keys s (execStmt o fs s stmt) := by + unfold prefixStmt at h + split at h + · rename_i n e + cases hv : evalExpr o fs s e <;> simp [execStmt, hv, Frame] + intro k hk + exact lookup_bind_other _ _ _ _ (by + intro heq; subst n + simp [hk] at h) + · rename_i n e + cases hv : evalExpr o fs s e <;> simp [execStmt, hv, Frame] + intro k hk + exact lookup_bind_other _ _ _ _ (by + intro heq; subst n + simp [hk] at h) + · rename_i cond yes no + have hh := Bool.and_eq_true_iff.mp h + cases hv : evalExpr o fs s cond + · simp [execStmt, hv, Frame] + · simp only [execStmt, hv] + split + · exact list_frame o fs s yes keys hh.1 + · exact list_frame o fs s no keys hh.2 + · simp [execStmt, Frame] + · cases h + theorem list_frame (o : DenoteOracle) (fs : List Field) (s : DenoteState) + (ss : List Stmt) (keys : List String) (h : prefixList keys ss = true) : + Frame keys s (execStmtList o fs s ss) := by + cases ss with + | nil => simp [execStmtList, Frame] + | cons stmt rest => + have hh := Bool.and_eq_true_iff.mp h + have hs := stmt_frame o fs s stmt keys hh.1 + simp only [execStmtList] + cases he : execStmt o fs s stmt + · rename_i t + have hf : t.world = s.world ∧ ∀ k ∈ keys, lookupValue t.bindings k = lookupValue s.bindings k := by + simpa [Frame, he] using hs + have hr := list_frame o fs t rest keys hh.2 + cases execStmtList o fs t rest <;> simp_all [Frame] + · simp_all [Frame] + · simp_all [Frame] + · trivial +end + +theorem exec_append (o : DenoteOracle) (fs : List Field) (s : DenoteState) + (a b : List Stmt) : execStmtList o fs s (a ++ b) = + match execStmtList o fs s a with + | .continue t => execStmtList o fs t b + | out => out := by + induction a generalizing s with + | nil => rfl + | cons stmt rest ih => + simp only [List.cons_append, execStmtList] + cases execStmt o fs s stmt <;> simp_all + +/-- A successful execution after a prefix must have continued through that prefix. -/ +theorem split_prefix (o : DenoteOracle) (fs : List Field) (s final : DenoteState) + (a b : List Stmt) (ha : prefixList [] a = true) + (h : execStmtList o fs s (a ++ b) = .stop final) : + ∃ t, execStmtList o fs s a = .continue t ∧ execStmtList o fs t b = .stop final := by + rw [exec_append] at h + have hf := list_frame o fs s a [] ha + cases he : execStmtList o fs s a <;> simp_all [Frame] + +theorem split_prefix_continue (o : DenoteOracle) (fs : List Field) (s final : DenoteState) + (a b : List Stmt) + (h : execStmtList o fs s (a ++ b) = .continue final) : + ∃ t, execStmtList o fs s a = .continue t ∧ execStmtList o fs t b = .continue final := by + rw [exec_append] at h + cases he : execStmtList o fs s a <;> simp_all + +theorem exec_let_cons (o : DenoteOracle) (fs : List Field) (s : DenoteState) + (n : String) (e : Expr) (ss : List Stmt) : + execStmtList o fs s (.letVar n e :: ss) = + match evalExpr o fs s e with + | some v => execStmtList o fs {s with bindings := bindValue s.bindings n v} ss + | none => .revert := by + simp only [execStmtList, execStmt] + cases evalExpr o fs s e <;> rfl + +@[simp] theorem exec_singleton (o : DenoteOracle) (fs : List Field) (s : DenoteState) + (stmt : Stmt) : execStmtList o fs s [stmt] = execStmt o fs s stmt := by + simp only [execStmtList] + cases execStmt o fs s stmt <;> rfl + +/-- A fall-through prefix followed by an explicit return can only stop or revert. -/ +theorem ends_return (o : DenoteOracle) (fs : List Field) (s : DenoteState) + (pre : List Stmt) (args : List Expr) (hp : prefixList [] pre = true) : + execStmtList o fs s (pre ++ [.returnValues args]) = .revert ∨ + ∃ t, execStmtList o fs s (pre ++ [.returnValues args]) = .stop t := by + rw [exec_append] + have hf := list_frame o fs s pre [] hp + cases he : execStmtList o fs s pre + · rename_i t + simp only [exec_singleton, execStmt] + cases evalExprList o fs t args <;> simp + · simp_all [Frame] + · simp_all [Frame] + · simp + +end Compiler.CompilationModel.SolidityImport diff --git a/Compiler/SolidityImport/Quote.lean b/Compiler/SolidityImport/Quote.lean new file mode 100644 index 0000000000..4c93af650e --- /dev/null +++ b/Compiler/SolidityImport/Quote.lean @@ -0,0 +1,153 @@ +import Lean +import Compiler.SolidityImport.Coverage +import Compiler.SolidityImport.Report + +/-! +Term syntax for the `CompilationModel` values the importer builds. + +The importer lowers Solidity to ordinary `CompilationModel` values and quotes +them here, so the elaborated definitions are the same terms a user would write. +Only the fragment the importer emits is quoted; anything else is an internal +error rather than a silently dropped field. `Stmt.ecm` holds functions and +cannot be quoted at all. +-/ + +open Lean + +namespace Compiler.CompilationModel.SolidityImport + +variable {m : Type → Type} [Monad m] [MonadQuotation m] [MonadError m] + +private def list (xs : List Term) : m Term := `([$(xs.toArray),*]) + +private def optNat : Option Nat → m Term + | none => `(none) + | some n => `(some $(quote n)) + +partial def quoteExpr : Expr → m Term + | .literal n => `(Compiler.CompilationModel.Expr.literal $(quote n)) + | .localVar x => `(Compiler.CompilationModel.Expr.localVar $(quote x)) + | .param x => `(Compiler.CompilationModel.Expr.param $(quote x)) + | .blockTimestamp => `(Compiler.CompilationModel.Expr.blockTimestamp) + | .structMember f k x => do + `(Compiler.CompilationModel.Expr.structMember $(quote f) $(← quoteExpr k) $(quote x)) + | .structMember2 f k1 k2 x => do + `(Compiler.CompilationModel.Expr.structMember2 $(quote f) $(← quoteExpr k1) $(← quoteExpr k2) $(quote x)) + | .add a b => do `(Compiler.CompilationModel.Expr.add $(← quoteExpr a) $(← quoteExpr b)) + | .sub a b => do `(Compiler.CompilationModel.Expr.sub $(← quoteExpr a) $(← quoteExpr b)) + | .mul a b => do `(Compiler.CompilationModel.Expr.mul $(← quoteExpr a) $(← quoteExpr b)) + | .div a b => do `(Compiler.CompilationModel.Expr.div $(← quoteExpr a) $(← quoteExpr b)) + | .bitAnd a b => do `(Compiler.CompilationModel.Expr.bitAnd $(← quoteExpr a) $(← quoteExpr b)) + | .bitXor a b => do `(Compiler.CompilationModel.Expr.bitXor $(← quoteExpr a) $(← quoteExpr b)) + | .eq a b => do `(Compiler.CompilationModel.Expr.eq $(← quoteExpr a) $(← quoteExpr b)) + | .lt a b => do `(Compiler.CompilationModel.Expr.lt $(← quoteExpr a) $(← quoteExpr b)) + | .gt a b => do `(Compiler.CompilationModel.Expr.gt $(← quoteExpr a) $(← quoteExpr b)) + | .le a b => do `(Compiler.CompilationModel.Expr.le $(← quoteExpr a) $(← quoteExpr b)) + | .ge a b => do `(Compiler.CompilationModel.Expr.ge $(← quoteExpr a) $(← quoteExpr b)) + | .logicalNot a => do `(Compiler.CompilationModel.Expr.logicalNot $(← quoteExpr a)) + | e => throwError "internal: the importer cannot quote {repr e}" + +private def quotePanic : Verity.Core.PanicCode → m Term + | .arithmeticOverflow => `(Verity.Core.PanicCode.arithmeticOverflow) + | .divisionByZero => `(Verity.Core.PanicCode.divisionByZero) + +partial def quoteStmt : Stmt → m Term + | .letVar x v => do `(Compiler.CompilationModel.Stmt.letVar $(quote x) $(← quoteExpr v)) + | .assignVar x v => do `(Compiler.CompilationModel.Stmt.assignVar $(quote x) $(← quoteExpr v)) + | .ite c t e => do + `(Compiler.CompilationModel.Stmt.ite $(← quoteExpr c) + $(← list (← t.mapM quoteStmt)) $(← list (← e.mapM quoteStmt))) + | .panic code => do `(Compiler.CompilationModel.Stmt.panic $(← quotePanic code)) + | .returnValues vs => do + `(Compiler.CompilationModel.Stmt.returnValues $(← list (← vs.mapM quoteExpr))) + | _ => throwError "internal: the importer cannot quote this statement" + +private def quoteKey : MappingKeyType → m Term + | .bytes32 => `(Compiler.CompilationModel.MappingKeyType.bytes32) + | .address => `(Compiler.CompilationModel.MappingKeyType.address) + | .uint256 => `(Compiler.CompilationModel.MappingKeyType.uint256) + +private def quoteMember (s : StructMember) : m Term := do + let ty ← match s.ty with + | .uint256 => `(Compiler.CompilationModel.StructMemberType.uint256) + | .uint16 => `(Compiler.CompilationModel.StructMemberType.uint16) + | t => throwError "internal: the importer cannot quote {repr t}" + let packed ← match s.packed with + | none => `(none) + | some p => `(some ({ offset := $(quote p.offset), width := $(quote p.width) } : + Compiler.CompilationModel.PackedBits)) + `(({ name := $(quote s.name), ty := $ty, wordOffset := $(quote s.wordOffset), packed := $packed } : + Compiler.CompilationModel.StructMember)) + +def quoteField (f : Field) : m Term := do + unless !f.isTransient && f.packedBits.isNone && f.aliasSlots.isEmpty do + throwError "internal: field {f.name} has settings the importer does not emit" + let ty ← match f.ty with + | .mappingStruct k ms => do + `(Compiler.CompilationModel.FieldType.mappingStruct $(← quoteKey k) $(← list (← ms.mapM quoteMember))) + | .mappingStruct2 k1 k2 ms => do + `(Compiler.CompilationModel.FieldType.mappingStruct2 $(← quoteKey k1) $(← quoteKey k2) + $(← list (← ms.mapM quoteMember))) + | t => throwError "internal: the importer cannot quote {repr t}" + `(({ name := $(quote f.name), ty := $ty, slot := $(← optNat f.slot) } : Compiler.CompilationModel.Field)) + +def quoteParamType : ParamType → m Term + | .uint256 => `(Compiler.CompilationModel.ParamType.uint256) + | .address => `(Compiler.CompilationModel.ParamType.address) + | .bytes32 => `(Compiler.CompilationModel.ParamType.bytes32) + | .bool => `(Compiler.CompilationModel.ParamType.bool) + | .uintN n => `(Compiler.CompilationModel.ParamType.uintN $(quote n)) + | t => throwError "internal: the importer cannot quote {repr t}" + +/-- Quote a function built by the importer. Only the fields it sets are +emitted; `quoteModel` callers check the round trip against the value. -/ +def quoteFunction (f : FunctionSpec) : m Term := do + let params ← f.params.mapM fun p => do + `(({ name := $(quote p.name), ty := $(← quoteParamType p.ty) } : Compiler.CompilationModel.Param)) + `(({ name := $(quote f.name), params := $(← list params), returnType := none, + returns := $(← list (← f.returns.mapM quoteParamType)), isView := $(quote f.isView), + body := $(← list (← f.body.mapM quoteStmt)) } : Compiler.CompilationModel.FunctionSpec)) + +def quoteModel (model : CompilationModel) : m Term := do + `(({ name := $(quote model.name), constructor := none, + fields := $(← list (← model.fields.mapM quoteField)), + functions := $(← list (← model.functions.mapM quoteFunction)) } : + Compiler.CompilationModel.CompilationModel)) + +private def strings (xs : List String) : m Term := list (xs.map quote) + +def quoteReport (r : ImportReport) : m Term := do + let fn (f : ImportedFunction) : m Term := do + `(({ contract := $(quote f.contract), name := $(quote f.name), declId := $(quote f.declId), + paramTypes := $(← strings f.paramTypes) } : Compiler.CompilationModel.SolidityImport.ImportedFunction)) + let proj (p : ParamProjection) : m Term := do + `(({ function := $(quote p.function), parameter := $(quote p.parameter), member := $(quote p.member), + structName := $(quote p.structName), + headWord := $(quote p.headWord), modelParam := $(quote p.modelParam), + ignoredMembers := $(← strings p.ignoredMembers) } : + Compiler.CompilationModel.SolidityImport.ParamProjection)) + let opq (o : OpaqueMember) : m Term := do + `(({ field := $(quote o.field), name := $(quote o.name), solcType := $(quote o.solcType), + wordOffset := $(quote o.wordOffset), byteOffset := $(quote o.byteOffset) } : + Compiler.CompilationModel.SolidityImport.OpaqueMember)) + let status (st : FunctionStatus) : m Term := do + let proof ← match st.compilerProof with + | .unavailable reason => + `(Compiler.CompilationModel.SolidityImport.CompilerProofStatus.unavailable $(quote reason)) + `(({ function := $(quote st.function), denoteCovered := $(quote st.denoteCovered), + compilerProof := $proof } : Compiler.CompilationModel.SolidityImport.FunctionStatus)) + `(({ importerVersion := $(quote r.importerVersion), solcLongVersion := $(quote r.solcLongVersion), + solcSha256 := $(quote r.solcSha256), settingsJson := $(quote r.settingsJson), + sourceDigest := $(quote r.sourceDigest), contract := $(quote r.contract), + roots := $(← strings r.roots), + functions := $(← list (← r.functions.mapM status)), + includedFunctions := $(← list (← r.includedFunctions.mapM fn)), + excludedFunctions := $(← list (← r.excludedFunctions.mapM fn)), + projections := $(← list (← r.projections.mapM proj)), + storageFields := $(← strings r.storageFields), + storageKeys := $(← list (← r.storageKeys.mapM fun (f, ks) => do `(($(quote f), $(← strings ks))))), + opaqueMembers := $(← list (← r.opaqueMembers.mapM opq)), + observesPanicPayload := $(quote r.observesPanicPayload) } : + Compiler.CompilationModel.SolidityImport.ImportReport)) + +end Compiler.CompilationModel.SolidityImport diff --git a/Compiler/SolidityImport/README.md b/Compiler/SolidityImport/README.md new file mode 100644 index 0000000000..1a5f230b5f --- /dev/null +++ b/Compiler/SolidityImport/README.md @@ -0,0 +1,3 @@ +# Solidity import + +See [docs/SOLIDITY_IMPORT.md](../../docs/SOLIDITY_IMPORT.md) for usage, the supported subset, the trust boundary and differential validation. diff --git a/Compiler/SolidityImport/Report.lean b/Compiler/SolidityImport/Report.lean new file mode 100644 index 0000000000..aff0f3714c --- /dev/null +++ b/Compiler/SolidityImport/Report.lean @@ -0,0 +1,112 @@ +/-! +Report of a Solidity function slice lowered to `CompilationModel`. + +A slice is not a whole-contract invariant. `opaqueMembers` are layout records +copied from solc and deliberately not given an executable encoding. +-/ +namespace Compiler.CompilationModel.SolidityImport + +/-- One function whose body was translated. Library helpers are included even +when the executable model inlines them. -/ +structure ImportedFunction where + contract : String + name : String + declId : Nat + paramTypes : List String + deriving Repr, BEq + +/-- A memory-struct parameter replaced by the static members the slice reads. +`headWord` is the member's index in the Solidity memory/ABI head, derived from +the struct declaration order. This is not an ABI decoder. -/ +structure ParamProjection where + /-- The imported function whose parameter is projected. -/ + function : String + parameter : String + member : String + structName : String + headWord : Nat + modelParam : String + ignoredMembers : List String + deriving Repr, BEq + +/-- A struct member present in solc's storage layout that this slice does not encode. -/ +structure OpaqueMember where + field : String + name : String + solcType : String + wordOffset : Nat + byteOffset : Nat + deriving Repr, BEq + +/-- What is known about a Verity compiler-correctness proof for one imported +function. There is no `covered` case: the importer has no decision procedure +for `SupportedSpec` and builds no witness, so it never claims coverage. -/ +inductive CompilerProofStatus where + /-- No compiler-correctness theorem is instantiated for this function. -/ + | unavailable (reason : String) + deriving Repr, BEq + +def CompilerProofStatus.toText : CompilerProofStatus → String + | .unavailable reason => s!"unavailable ({reason})" + +/-- Why `compilerProof` is `unavailable` for every imported function. -/ +def noCompilerProofReason : String := + "the importer builds no SupportedSpec witness; compiled code is only tested by A/B/C" + +/-- Static status of one root. A root is listed only if it was importable. +`compilable` is not recorded here: it depends on the compiler, not the import, +and `Differential` reports it next to these fields. -/ +structure FunctionStatus where + function : String + /-- `stmtListCovered` holds on the lowered body. The kernel-checked form of + this fact, for all roots together, is the theorem `x.covered`. -/ + denoteCovered : Bool + compilerProof : CompilerProofStatus + deriving Repr, BEq + +/-- Provenance and closure report. `sourceDigest` covers sources, solc settings, +the compiler release identity, and the importer sources. -/ +structure ImportReport where + importerVersion : String + solcLongVersion : String + solcSha256 : String + settingsJson : String + sourceDigest : String + contract : String + /-- The imported functions, in `solidity_import` order. -/ + roots : List String + /-- One status per root, in `roots` order. -/ + functions : List FunctionStatus + includedFunctions : List ImportedFunction + excludedFunctions : List ImportedFunction + projections : List ParamProjection + storageFields : List String + /-- Solidity names of each storage field's mapping keys (`""` when unnamed). -/ + storageKeys : List (String × List String) := [] + opaqueMembers : List OpaqueMember + /-- Proof denotation records success versus revert. `Stmt.panic` keeps its + `PanicCode` on the statement, and `execStmt` does not return that payload. -/ + observesPanicPayload : Bool := false + deriving Repr, BEq + +/-- Stable, reviewable inventory of the translated closure and explicit exclusions. -/ +def ImportReport.toText (r : ImportReport) : String := Id.run do + let mut lines := [s!"importer {r.importerVersion}", s!"solc {r.solcLongVersion}", + s!"solcSha256 {r.solcSha256}", s!"digest {r.sourceDigest}", + s!"contract {r.contract}"] ++ r.roots.map (s!"root {·}") ++ [s!"settings {r.settingsJson}"] + for st in r.functions do + lines := lines ++ [s!"status {st.function} importable denoteCovered {st.denoteCovered} compilerProof {st.compilerProof.toText}"] + for fn in r.includedFunctions do + lines := lines ++ [s!"include {fn.contract}.{fn.name} decl {fn.declId} params {fn.paramTypes}"] + for fn in r.excludedFunctions do + lines := lines ++ [s!"exclude {fn.contract}.{fn.name} decl {fn.declId} params {fn.paramTypes}"] + for p in r.projections do + lines := lines ++ [s!"projection {p.function} {p.parameter}.{p.member} head {p.headWord} as {p.modelParam} ignored {p.ignoredMembers}"] + for name in r.storageFields do + let keys := (r.storageKeys.find? (·.1 == name)).map (·.2) |>.getD [] + lines := lines ++ [s!"storage {name} keys {keys}"] + for m in r.opaqueMembers do + lines := lines ++ [s!"opaque {m.field}.{m.name} {m.solcType} word {m.wordOffset} byte {m.byteOffset}"] + return String.intercalate "\n" lines ++ "\n" + +end Compiler.CompilationModel.SolidityImport diff --git a/Compiler/TypedIRCompilerCorrectness.lean b/Compiler/TypedIRCompilerCorrectness.lean index 8c92c37885..e8a80cfbea 100644 --- a/Compiler/TypedIRCompilerCorrectness.lean +++ b/Compiler/TypedIRCompilerCorrectness.lean @@ -6352,9 +6352,7 @@ theorem witness_abiHeadParamShapes_compile : /-! `getApproved` and `approve` depend on the live `ownersSlot = 4` / `tokenApprovalsSlot = 5` layout and include multi-step owner guards. They do -not fit the older single-fragment witness surface used above. Likewise, the -broader ABI-head roundtrip/lowering regressions for tuple/bytes/array/string -params live in `Contracts/TypedIRTests.lean`. +not fit the older single-fragment witness surface used above. -/ -- ============================================================================ diff --git a/Contracts/Legacy.lean b/Contracts/Legacy.lean deleted file mode 100644 index cf111bedd2..0000000000 --- a/Contracts/Legacy.lean +++ /dev/null @@ -1,3 +0,0 @@ -import Contracts.Specs - -/-! Deprecated compatibility import for the canonical contract specs. -/ diff --git a/Contracts/SolidityImportSmoke/Inheritance/Inheritance.lean b/Contracts/SolidityImportSmoke/Inheritance/Inheritance.lean deleted file mode 100644 index ca5f22250c..0000000000 --- a/Contracts/SolidityImportSmoke/Inheritance/Inheritance.lean +++ /dev/null @@ -1,7 +0,0 @@ -import Contracts.VaultFromSolidity.Importer.Importer - -namespace Contracts.SolidityImportSmoke.Inheritance - -solidity_contract Child from "Inheritance.sol" - -end Contracts.SolidityImportSmoke.Inheritance diff --git a/Contracts/SolidityImportSmoke/Inheritance/Inheritance.sol b/Contracts/SolidityImportSmoke/Inheritance/Inheritance.sol deleted file mode 100644 index 6df6cad19f..0000000000 --- a/Contracts/SolidityImportSmoke/Inheritance/Inheritance.sol +++ /dev/null @@ -1,58 +0,0 @@ -// SPDX-License-Identifier: MIT -pragma solidity ^0.8.33; - -/// Synthetic Storage-like / Pausable-like / Ownable-like bases plus a diamond, -/// used as the S1 inheritance smoke. Only POC types plus `address` scalars. -abstract contract Base { - uint256 public baseValue; - - function _bump() internal virtual { - baseValue += 1; - } -} - -abstract contract Left is Base { - uint256 public leftValue; -} - -abstract contract Right is Base { - uint256 public rightValue; -} - -abstract contract PausableLike { - uint256 public paused; - - function _pause() internal virtual { - paused = 1; - } - - function pause() external { - _pause(); - } -} - -abstract contract OwnableLike { - address public owner; -} - -contract Child is Left, Right, PausableLike, OwnableLike { - uint256 public childValue; - bytes32 private unusedGap; - - function _pause() internal virtual override { - super._pause(); - childValue += 1; - } - - function go() external { - _pause(); - } - - function bump() external { - _bump(); - } - - function _add(uint256 a, uint256 b) internal pure returns (uint256) { - return a + b; - } -} diff --git a/Contracts/SolidityImportSmoke/Inheritance/Proofs.lean b/Contracts/SolidityImportSmoke/Inheritance/Proofs.lean deleted file mode 100644 index 3d77c57f12..0000000000 --- a/Contracts/SolidityImportSmoke/Inheritance/Proofs.lean +++ /dev/null @@ -1,123 +0,0 @@ -import Contracts.SolidityImportSmoke.Inheritance.Spec -import Verity.Proofs.Stdlib.SolidityImport - -namespace Contracts.SolidityImportSmoke.Inheritance.Proofs -open Verity -open Verity.Stdlib.Math -open Verity.Core.Invariant -open Verity.Proofs.Stdlib.SolidityImport -open Contracts.SolidityImportSmoke.Inheritance.Child -open Spec - -theorem pause_success_spec (s post : ContractState) - (h : PausableLike_pause.run s = .success () post) : - pause_spec (view s) (view post) := by - unfold pause_spec - by_cases h0 : s.msgValue = 0 - · by_cases hcv : (view s).childValue.val + 1 ≤ Verity.Core.MAX_UINT256 - · solidity_simp - subst h - solidity_simp - · solidity_simp - · solidity_simp - -theorem go_success_spec (s post : ContractState) - (h : go.run s = .success () post) : - pause_spec (view s) (view post) := by - unfold pause_spec - by_cases h0 : s.msgValue = 0 - · by_cases hcv : (view s).childValue.val + 1 ≤ Verity.Core.MAX_UINT256 - · solidity_simp - subst h - solidity_simp - · solidity_simp - · solidity_simp - -theorem bump_success_spec (s post : ContractState) - (h : bump.run s = .success () post) : - bump_spec (view s) (view post) := by - unfold bump_spec - by_cases h0 : s.msgValue = 0 - · by_cases hb : (view s).baseValue.val + 1 ≤ Verity.Core.MAX_UINT256 - · solidity_simp - subst h - solidity_simp - · solidity_simp - · solidity_simp - -/-- A base entry point that calls the virtual observes the child's override. -/ -theorem dispatch_is_child (s post : ContractState) - (h : PausableLike_pause.run s = .success () post) : - (view post).paused = 1 ∧ (view post).childValue = (view s).childValue + 1 := by - have hspec := pause_success_spec s post h - exact ⟨hspec.1, hspec.2.1⟩ - -/-- `go()` calls `Child._pause`, whose `super._pause()` runs the parent body. -/ -theorem super_runs_parent (s post : ContractState) - (h : go.run s = .success () post) : - (view post).paused = 1 := - (go_success_spec s post h).1 - -theorem pause_meets_spec (s : ContractState) - (h0 : s.msgValue = 0) (hfits : pauseFits (view s)) : - ∃ post, PausableLike_pause.run s = ContractResult.success () post ∧ - pause_spec (view s) (view post) := by - unfold pause_spec - solidity_simp [pauseFits] - -theorem go_meets_spec (s : ContractState) - (h0 : s.msgValue = 0) (hfits : pauseFits (view s)) : - ∃ post, go.run s = ContractResult.success () post ∧ - pause_spec (view s) (view post) := by - unfold pause_spec - solidity_simp [pauseFits] - -theorem bump_meets_spec (s : ContractState) - (h0 : s.msgValue = 0) (hfits : bumpFits (view s)) : - ∃ post, bump.run s = ContractResult.success () post ∧ - bump_spec (view s) (view post) := by - unfold bump_spec - solidity_simp [bumpFits] - -/-- `step` is go, bump, PausableLike_pause, then getters in layout order. -/ -theorem paused_invariant : PreservedBy (fun s => pausedFlag (view s)) step := by - intro s s' hflag hstep - rcases hstep with hgo | hbump | hpause | hbase | hleft | hright | hpaused | howner | hchild - · subst hgo - cases run_snd_cases go s with - | inl hrev => rw [hrev]; exact hflag - | inr hsucc => - rcases hsucc with ⟨_, post, hrun, hsnd⟩ - rw [hsnd] - exact Or.inr (go_success_spec s post hrun).1 - · subst hbump - cases run_snd_cases bump s with - | inl hrev => rw [hrev]; exact hflag - | inr hsucc => - rcases hsucc with ⟨_, post, hrun, hsnd⟩ - rw [hsnd] - have hp := (bump_success_spec s post hrun).2.2.1 - unfold pausedFlag at hflag ⊢ - rw [hp] - exact hflag - · subst hpause - cases run_snd_cases PausableLike_pause s with - | inl hrev => rw [hrev]; exact hflag - | inr hsucc => - rcases hsucc with ⟨_, post, hrun, hsnd⟩ - rw [hsnd] - exact Or.inr (pause_success_spec s post hrun).1 - · subst hbase - by_cases h0 : s.msgValue = 0 <;> solidity_simp [pausedFlag] - · subst hleft - by_cases h0 : s.msgValue = 0 <;> solidity_simp [pausedFlag] - · subst hright - by_cases h0 : s.msgValue = 0 <;> solidity_simp [pausedFlag] - · subst hpaused - by_cases h0 : s.msgValue = 0 <;> solidity_simp [pausedFlag] - · subst howner - by_cases h0 : s.msgValue = 0 <;> solidity_simp [pausedFlag] - · subst hchild - by_cases h0 : s.msgValue = 0 <;> solidity_simp [pausedFlag] - -end Contracts.SolidityImportSmoke.Inheritance.Proofs diff --git a/Contracts/SolidityImportSmoke/Inheritance/Spec.lean b/Contracts/SolidityImportSmoke/Inheritance/Spec.lean deleted file mode 100644 index 11ba8ffb33..0000000000 --- a/Contracts/SolidityImportSmoke/Inheritance/Spec.lean +++ /dev/null @@ -1,44 +0,0 @@ -import Verity.EVM.Uint256 -import Contracts.SolidityImportSmoke.Inheritance.Inheritance - -/-! -Named-storage promises for the S1 inheritance smoke. `paused` is a 0/1 flag -set by `_pause`; `childValue` counts how many times the child's override ran. --/ - -namespace Contracts.SolidityImportSmoke.Inheritance.Spec - -open Verity -open Verity.EVM.Uint256 -open Contracts.SolidityImportSmoke.Inheritance.Child - -/-- `paused` is a boolean encoded as `uint256` 0 or 1. -/ -def pausedFlag (v : Storage) : Prop := - v.paused = 0 ∨ v.paused = 1 - -/-- `pause()` / `go()` run the child's `_pause` override: parent sets `paused` -and the child increments `childValue`. -/ -def pause_spec (pre post : Storage) : Prop := - post.paused = 1 ∧ - post.childValue = pre.childValue + 1 ∧ - post.baseValue = pre.baseValue ∧ - post.leftValue = pre.leftValue ∧ - post.rightValue = pre.rightValue ∧ - post.owner = pre.owner - -/-- `bump()` runs `Base._bump`. -/ -def bump_spec (pre post : Storage) : Prop := - post.baseValue = pre.baseValue + 1 ∧ - post.childValue = pre.childValue ∧ - post.paused = pre.paused ∧ - post.leftValue = pre.leftValue ∧ - post.rightValue = pre.rightValue ∧ - post.owner = pre.owner - -def pauseFits (v : Storage) : Prop := - v.childValue.val + 1 ≤ Verity.Core.MAX_UINT256 - -def bumpFits (v : Storage) : Prop := - v.baseValue.val + 1 ≤ Verity.Core.MAX_UINT256 - -end Contracts.SolidityImportSmoke.Inheritance.Spec diff --git a/Contracts/SolidityImportSmoke/Inheritance/scripts/inheritance_test.py b/Contracts/SolidityImportSmoke/Inheritance/scripts/inheritance_test.py deleted file mode 100644 index 1d5e128763..0000000000 --- a/Contracts/SolidityImportSmoke/Inheritance/scripts/inheritance_test.py +++ /dev/null @@ -1,381 +0,0 @@ -#!/usr/bin/env python3 -"""Acceptance checks for the Solidity importer inheritance slice.""" - -import hashlib -import os -from pathlib import Path -import re -import shutil -import subprocess -import tempfile - -ROOT = Path(__file__).resolve().parents[4] - - -def lake_binary() -> str: - found = shutil.which("lake") - if found: - return found - version = (ROOT / "lean-toolchain").read_text().strip().split(":")[-1] - candidates = [ - Path.home() / ".elan/toolchains" / f"leanprover--lean4---{version}" / "bin/lake", - Path("/var/lib/opencode/.elan/toolchains") / f"leanprover--lean4---{version}" / "bin/lake", - ] - for candidate in candidates: - if candidate.is_file(): - return str(candidate) - raise RuntimeError("lake executable not found") - - -LAKE = lake_binary() -ENV = dict(os.environ) -SMOKE = "Contracts/SolidityImportSmoke/Inheritance" -TARGET = "SolidityImportSmokeInheritance" -NS = "Contracts.SolidityImportSmoke.Inheritance.Child" -PROOF = f"{SMOKE}/Proofs.lean" - - -def check(ok: bool, message: str) -> None: - if not ok: - raise AssertionError(message) - print("PASS " + message, flush=True) - - -def run(root: Path, args: list[str], success: bool = True, contains: str | None = None) -> str: - process = subprocess.run( - args, cwd=root, env=ENV, text=True, capture_output=True, timeout=300 - ) - output = process.stdout + process.stderr - if (process.returncode == 0) != success or (contains and contains not in output) or "PANIC" in output: - raise AssertionError(f"{args}: exit {process.returncode}\n{output}") - return output - - -def main() -> None: - importer_path = ROOT / "Contracts/VaultFromSolidity/Importer/Importer.lean" - syntax_path = ROOT / "Contracts/VaultFromSolidity/Importer/Syntax.lean" - semantics_path = ROOT / "Contracts/VaultFromSolidity/Importer/Semantics.lean" - importer_text = importer_path.read_text() - syntax_text = syntax_path.read_text() - semantics_text = semantics_path.read_text() - check("inductive FVar" in syntax_text and "inductive Args" in syntax_text, - "function table (FVar/Args) is in Syntax.lean") - check("| call " in syntax_text and "| callStmt" in syntax_text, - "internal calls are Expr.call / Stmt.callStmt") - check("FnEnv" in semantics_text and "Args.eval" in semantics_text, - "call meaning is in Semantics.lean") - check("registeredSources" in importer_text and "linearizedBaseContracts" in importer_text, - "importer accepts C3 linearization and a source manifest") - check("effectful internal call in expression position" in importer_text, - "importer rejects effectful Expr.call (legacy codegen order)") - check("super dispatch family mismatch" in importer_text and - "super dispatch disagrees with AST" not in importer_text, - "super follows target C3 rather than defining-contract AST id") - - with tempfile.TemporaryDirectory(prefix="verity-inh-check-", dir=ROOT.parent) as directory: - root = Path(directory) - for name in ("Verity", "Compiler", "Contracts", "scripts"): - shutil.copytree(ROOT / name, root / name) - for name in ("lakefile.lean", "lake-manifest.json", "lean-toolchain"): - shutil.copy2(ROOT / name, root / name) - shutil.copytree(ROOT / ".lake/build", root / ".lake/build") - (root / ".lake/solidity-import").mkdir(parents=True) - shutil.copy2(ROOT / ".lake/solidity-import/solc", root / ".lake/solidity-import/solc") - (root / ".lake/packages").symlink_to(ROOT / ".lake/packages", target_is_directory=True) - - source = root / SMOKE / "Inheritance.sol" - original = source.read_bytes() - source_stamp = source.stat() - importer = root / "Contracts/VaultFromSolidity/Importer/Importer.lean" - importer_original = importer.read_bytes() - importer_stamp = importer.stat() - proof = root / PROOF - proof_text = proof.read_text() - theorem_names = re.findall(r"^theorem\s+(\w+)", proof_text, re.M) - - def edit_source(data: bytes) -> None: - source.write_bytes(data) - os.utime(source, ns=(source_stamp.st_atime_ns, source_stamp.st_mtime_ns)) - - def build(success: bool = True, contains: str | None = None) -> str: - return run(root, [LAKE, "build", TARGET], success, contains) - - def artifacts() -> dict[str, tuple[int, str]]: - paths = list((root / ".lake/build/lib/lean/Contracts/SolidityImportSmoke/Inheritance").rglob("*.olean")) - return { - str(path.relative_to(root)): ( - path.stat().st_mtime_ns, - hashlib.sha256(path.read_bytes()).hexdigest(), - ) - for path in paths - } - - def source_digest() -> str: - digest_probe = root / ".lake/solidity-import/InhDigestProbe.lean" - try: - digest_probe.write_text( - f"import {SMOKE.replace('/', '.')}.Inheritance\n" - f"#eval {NS}.sourceDigest\n" - ) - output = run(root, [LAKE, "env", "lean", str(digest_probe)]) - match = re.search(r'"([0-9a-f]{64})"', output) - if match is None: - raise AssertionError("sourceDigest is not an auditable SHA-256 value") - return match.group(1) - finally: - digest_probe.unlink(missing_ok=True) - - def slot_numbers() -> str: - slot_probe = root / ".lake/solidity-import/InhSlotProbe.lean" - try: - slot_probe.write_text( - f"import {SMOKE.replace('/', '.')}.Inheritance\n" - f"#eval [{NS}.baseValueSlot.slot, {NS}.leftValueSlot.slot, " - f"{NS}.rightValueSlot.slot, {NS}.pausedSlot.slot, " - f"{NS}.ownerSlot.slot, {NS}.childValueSlot.slot]\n" - ) - found = re.findall(r"\[\d+(?:, \d+)*\]", run(root, [LAKE, "env", "lean", str(slot_probe)])) - if not found: - raise AssertionError("slot probe produced no list") - return found[0] - finally: - slot_probe.unlink(missing_ok=True) - - build() - check(True, "baseline lake build SolidityImportSmokeInheritance") - - audit_file = root / ".lake/solidity-import/InhAxiomAudit.lean" - try: - audit_file.write_text( - f"import {SMOKE.replace('/', '.')}.Proofs\n" - + "\n".join( - f"#print axioms {SMOKE.replace('/', '.')}.Proofs.{name}" - for name in theorem_names - ) - + "\n" - ) - audit = run(root, [LAKE, "env", "lean", str(audit_file)]) - finally: - audit_file.unlink(missing_ok=True) - entries = re.findall( - r"'Contracts.SolidityImportSmoke.Inheritance.Proofs.(\w+)' depends on axioms: \[([^\]]*)\]", - audit, - ) - check(set(theorem_names) == {name for name, _ in entries}, - "every inheritance theorem appears in #print axioms output") - axioms = {a.strip() for _, values in entries for a in values.split(",") if a.strip()} - check(axioms <= {"propext", "Quot.sound"}, - "no project axioms or sorryAx: " + ", ".join(sorted(axioms))) - - print_probe = root / ".lake/solidity-import/InhPrintProbe.lean" - try: - print_probe.write_text( - f"import {SMOKE.replace('/', '.')}.Inheritance\n" - f"#print {NS}.go\n" - f"#print {NS}.Child__pause\n" - ) - printed = run(root, [LAKE, "env", "lean", str(print_probe)]) - check("Child__pause" in printed and "callStmt" in printed, - "#print Child.go shows a call to Child__pause") - check("PausableLike__pause" in printed, - "#print Child__pause shows the super hop to PausableLike__pause") - finally: - print_probe.unlink(missing_ok=True) - - def import_only(success: bool = True, contains: str | None = None) -> str: - # Rebuild the importer module without Proofs.lean: diamond internals - # enlarge FnEnv enough that the smoke proofs' `simp` budget fails, - # but solc C3 super still has to elaborate. - return run( - root, - [LAKE, "build", "Contracts.SolidityImportSmoke.Inheritance.Inheritance"], - success, - contains, - ) - - def print_names(*names: str) -> str: - probe = root / ".lake/solidity-import/InhPrintNames.lean" - try: - probe.write_text( - f"import {SMOKE.replace('/', '.')}.Inheritance\n" - + "".join(f"#print {NS}.{name}\n" for name in names) - ) - return run(root, [LAKE, "env", "lean", str(probe)]) - finally: - probe.unlink(missing_ok=True) - - slots = slot_numbers() - check(slots == "[0, 1, 2, 3, 4, 5]", - "inherited slots follow solc layout [base, left, right, paused, owner, child]") - - swapped = original.replace(b"is Left, Right, PausableLike, OwnableLike", - b"is Right, Left, PausableLike, OwnableLike") - check(original != swapped, "is-order mutation has a source target") - edit_source(swapped) - build() - check(slot_numbers() == "[0, 2, 1, 3, 4, 5]", - "swapping is-order changes C3 and inherited slot numbers") - edit_source(original) - build() - - diamond = original.replace( - b" function _bump() internal virtual {\n baseValue += 1;\n }\n", - b" function _bump() internal virtual {\n baseValue += 1;\n }\n" - b" function _trace() internal virtual {}\n", - ).replace( - b"abstract contract Left is Base {\n uint256 public leftValue;\n}\n", - b"abstract contract Left is Base {\n uint256 public leftValue;\n" - b" function _trace() internal virtual override { super._trace(); }\n}\n", - ).replace( - b"abstract contract Right is Base {\n uint256 public rightValue;\n}\n", - b"abstract contract Right is Base {\n uint256 public rightValue;\n" - b" function _trace() internal virtual override {}\n}\n", - ).replace( - b" function go() external {\n _pause();\n }\n", - b" function _trace() internal override(Left, Right) { super._trace(); }\n" - b" function go() external {\n _pause();\n }\n", - ) - check(original != diamond, "diamond virtual mutation has a source target") - edit_source(diamond) - import_only() - diamond_printed = print_names("Child__trace", "Left__trace") - check("callStmt" in diamond_printed, - "diamond super on default is-order imports as callStmt") - # `is Right, Left` makes Left more derived than Right. Left._trace's AST - # super target is Base._trace; target C3 super is Right._trace. - diamond_swapped = diamond.replace( - b"is Left, Right, PausableLike, OwnableLike", - b"is Right, Left, PausableLike, OwnableLike") - edit_source(diamond_swapped) - swapped_import = import_only() - check("super dispatch disagrees with AST" not in swapped_import, - "diamond super follows target C3, not defining-contract AST id") - check("callStmt" in print_names("Left__trace"), - "#print Left__trace after diamond is-order swap still has super callStmt") - edit_source(original) - build() - - sum_src = original.replace( - b" function _add(uint256 a, uint256 b) internal pure returns (uint256) {\n" - b" return a + b;\n }\n", - b" function _add(uint256 a, uint256 b) internal pure returns (uint256) {\n" - b" return a + b;\n }\n" - b" function _sum() internal pure returns (uint256) {\n" - b" return _add(1, 2);\n }\n", - ) - check(original != sum_src, "pure Expr.call mutation has a source target") - edit_source(sum_src) - import_only() - check("Child__add" in print_names("Child__sum"), - "#print Child__sum shows Expr.call of pure Child__add") - edit_source(original) - build() - - def broken_theorems(output: str) -> set[str]: - error_lines = [int(value) for value in re.findall(rf"{PROOF}:(\d+):", output)] - starts = [ - (match.group(1), line_no) - for line_no, line in enumerate(proof_text.splitlines(), 1) - if (match := re.match(r"theorem\s+([A-Za-z0-9_']+)", line.strip())) - ] - ranges = { - name: (start, starts[index + 1][1] - 1 if index + 1 < len(starts) - else len(proof_text.splitlines())) - for index, (name, start) in enumerate(starts) - } - return {name for name, (start, end) in ranges.items() - if any(start <= line <= end for line in error_lines)} - - override = b" childValue += 1;" - check(original.count(override) == 1, "override body has one source target") - edit_source(original.replace(override, b" childValue = 0;")) - output = build(False, "Contracts.SolidityImportSmoke.Inheritance.Proofs") - broken = broken_theorems(output) - check(bool({"dispatch_is_child", "pause_success_spec", "go_success_spec"} & broken), - "changing the override body breaks the dispatch observation") - edit_source(original) - build() - - pause_fn = b" function pause() external {\n _pause();\n }\n" - extra = b" function ping() external {\n paused = paused;\n }\n" + pause_fn - check(original.count(pause_fn) == 1, "base public function has one source target") - edit_source(original.replace(pause_fn, extra)) - output = build(False, "Contracts.SolidityImportSmoke.Inheritance.Proofs") - check("paused_invariant" in broken_theorems(output), - "adding a public function to a base breaks paused_invariant") - edit_source(original) - build() - - before = artifacts() - digest_before = source_digest() - importer.write_bytes(importer_original + b"\n-- inheritance identity probe\n") - os.utime(importer, ns=(importer_stamp.st_atime_ns, importer_stamp.st_mtime_ns)) - build() - check(before != artifacts(), "Lean importer content change invalidates inheritance artifacts") - check(digest_before != source_digest(), "Lean importer content changes sourceDigest") - importer.write_bytes(importer_original) - build() - - semantics = root / "Contracts/VaultFromSolidity/Importer/Semantics.lean" - semantics_original = semantics.read_bytes() - semantics_stamp = semantics.stat() - before = artifacts() - digest_before = source_digest() - semantics.write_bytes(semantics_original + b"\n-- inheritance semantics identity probe\n") - os.utime(semantics, ns=(semantics_stamp.st_atime_ns, semantics_stamp.st_mtime_ns)) - build() - check(before != artifacts(), "Lean semantics content change invalidates inheritance artifacts") - check(digest_before != source_digest(), "Lean semantics content changes sourceDigest") - semantics.write_bytes(semantics_original) - build() - - for name, old, new, diagnostic in ( - ("direct recursion", - b" super._pause();\n childValue += 1;", - b" _pause();\n childValue += 1;", - "recursion"), - ("unimplemented virtual", - b" function _bump() internal virtual {\n baseValue += 1;\n }", - b" function _bump() internal virtual;", - "abstract"), - ("opaque field touch", - b" function go() external {\n _pause();\n }", - b" function go() external {\n unusedGap = unusedGap;\n _pause();\n }", - "opaque field"), - ("function overloading", - b" function bump() external {\n _bump();\n }", - b" function bump() external {\n _bump();\n }\n" - b" function bump(uint256 x) external {\n baseValue = x;\n }", - "overloading"), - ("effectful add of internal call", - b" function bump() external {\n _bump();\n }", - b" function _inc() internal returns (uint256) {\n" - b" childValue += 1;\n return childValue;\n }\n" - b" function bump() external {\n" - b" childValue = childValue + _inc();\n }", - "effectful internal call in expression position"), - ("effectful compound assignment of internal call", - b" function bump() external {\n _bump();\n }", - b" function _inc() internal returns (uint256) {\n" - b" childValue += 1;\n return childValue;\n }\n" - b" function bump() external {\n" - b" childValue += _inc();\n }", - "effectful internal call in expression position"), - ): - check(original.count(old) == 1, name + " mutation has one source target") - edit_source(original.replace(old, new)) - output = build(False, diagnostic) - check(re.search(r"Contracts/SolidityImportSmoke/Inheritance/Inheritance.sol:\d+:\d+:", output) - is not None, - name + " rejected with source position") - edit_source(original) - build() - - check(source.read_bytes() == original and importer.read_bytes() == importer_original, - "temporary mutations restored; final baseline passes") - print(f"PASS all inheritance-slice acceptance checks ({len(theorem_names)} audited theorems)", - flush=True) - - -if __name__ == "__main__": - main() diff --git a/Contracts/SolidityImportSmoke/Lib.sol b/Contracts/SolidityImportSmoke/Lib.sol new file mode 100644 index 0000000000..e72cb2c657 --- /dev/null +++ b/Contracts/SolidityImportSmoke/Lib.sol @@ -0,0 +1,26 @@ +// SPDX-License-Identifier: MIT +pragma solidity 0.8.34; + +library L { + function min(uint256 x, uint256 y) internal pure returns (uint256 z) { + assembly { + z := xor(x, mul(xor(x, y), lt(y, x))) + } + } + + function mulDivDown(uint256 x, uint256 y, uint256 d) internal pure returns (uint256) { + return (x * y) / d; + } + + function mulDivUp(uint256 x, uint256 y, uint256 d) internal pure returns (uint256) { + return (x * y + (d - 1)) / d; + } + + function unused() internal pure returns (uint256) { + uint256 s = 0; + for (uint256 i = 0; i < 3; i++) { + s += i; + } + return s; + } +} diff --git a/Contracts/SolidityImportSmoke/Modifiers/Modifiers.lean b/Contracts/SolidityImportSmoke/Modifiers/Modifiers.lean deleted file mode 100644 index 0738eb2da4..0000000000 --- a/Contracts/SolidityImportSmoke/Modifiers/Modifiers.lean +++ /dev/null @@ -1,7 +0,0 @@ -import Contracts.VaultFromSolidity.Importer.Importer - -namespace Contracts.SolidityImportSmoke.Modifiers - -solidity_contract Child from "Modifiers.sol" - -end Contracts.SolidityImportSmoke.Modifiers diff --git a/Contracts/SolidityImportSmoke/Modifiers/Modifiers.sol b/Contracts/SolidityImportSmoke/Modifiers/Modifiers.sol deleted file mode 100644 index 4f66957a90..0000000000 --- a/Contracts/SolidityImportSmoke/Modifiers/Modifiers.sol +++ /dev/null @@ -1,93 +0,0 @@ -// SPDX-License-Identifier: MIT -pragma solidity ^0.8.33; - -error NotOwner(); -error EnforcedPause(); -error ReentrancyGuardReentrantCall(); - -abstract contract OwnableLike { - address public owner; - - modifier onlyOwner() { - if (msg.sender != owner) revert NotOwner(); - _; - } -} - -abstract contract PausableLike { - uint256 public paused; - - modifier whenNotPaused() { - if (0 < paused) revert EnforcedPause(); - _; - } -} - -abstract contract ReentrancyGuardLike { - uint256 public status; - - modifier nonReentrant() { - if (1 < status) revert ReentrancyGuardReentrantCall(); - status = 2; - _; - status = 1; - } - - modifier restore() { - uint256 old = status; - _; - status = old; - } -} - -abstract contract HelperLike { - uint256 public helperValue; - - modifier bumpHelper() { - bump(); - _; - } - - function bump() private { - helperValue = 1; - } -} - -contract Child is OwnableLike, PausableLike, ReentrancyGuardLike, HelperLike { - uint256 public value; - - function setOwner(address next) external { - owner = next; - } - - function arm() external { - status = 1; - } - - function guarded(uint256 amount) external onlyOwner whenNotPaused { - value = amount; - } - - function enter(uint256 amount) external nonReentrant returns (uint256) { - value = amount; - return amount; - } - - function early(uint256 amount) external nonReentrant returns (uint256) { - value = amount; - return amount; - } - - function poison() private { - helperValue = 2; - } - - function tagged() external bumpHelper { - value = 0; - } - - function snapshot(uint256 amount) external restore returns (uint256) { - status = amount; - return amount; - } -} diff --git a/Contracts/SolidityImportSmoke/Modifiers/Proofs.lean b/Contracts/SolidityImportSmoke/Modifiers/Proofs.lean deleted file mode 100644 index 62c850e97a..0000000000 --- a/Contracts/SolidityImportSmoke/Modifiers/Proofs.lean +++ /dev/null @@ -1,75 +0,0 @@ -import Contracts.SolidityImportSmoke.Modifiers.Spec -import Verity.Proofs.Stdlib.SolidityImport - -namespace Contracts.SolidityImportSmoke.Modifiers.Proofs -open Verity -open Verity.Proofs.Stdlib.SolidityImport -open Contracts.SolidityImportSmoke.Modifiers.Child -open Spec - -theorem status_restored (s post : ContractState) (amount : Uint256) - (h : (early amount).run s = .success amount post) : - early_spec amount (view s) (view post) := by - unfold early_spec - by_cases h0 : s.msgValue = 0 - · by_cases hst : 1 < (view s).status.val - · solidity_simp - · solidity_simp - subst h - solidity_simp - · solidity_simp - -theorem early_meets_spec (s : ContractState) (amount : Uint256) - (h0 : s.msgValue = 0) (harmed : (view s).status = 1) : - ∃ post, (early amount).run s = ContractResult.success amount post ∧ - early_spec amount (view s) (view post) := by - unfold early_spec - solidity_simp - -theorem guarded_success_spec (s post : ContractState) (amount : Uint256) - (h : (guarded amount).run s = .success () post) : - guarded_spec amount (view s) (view post) := by - unfold guarded_spec - by_cases h0 : s.msgValue = 0 - · by_cases ho : s.sender = (view s).owner - · by_cases hp : (view s).paused.val = 0 - · solidity_simp - subst h - solidity_simp - · solidity_simp - · by_cases hp : (view s).paused.val = 0 - · solidity_simp - · solidity_simp - · solidity_simp - -theorem guarded_reverts_not_owner_before_pause (s : ContractState) (amount : Uint256) - (h0 : s.msgValue = 0) - (howner : s.sender ≠ (view s).owner) - (hpaused : 0 < (view s).paused.val) : - (guarded amount).run s = ContractResult.revert "NotOwner()" s := by - by_cases ho : s.sender = (view s).owner - · exact (howner ho).elim - · have := hpaused - solidity_simp - -theorem tagged_uses_base_helper (s post : ContractState) - (h : tagged.run s = .success () post) : - tagged_spec (view s) (view post) := by - unfold tagged_spec - by_cases h0 : s.msgValue = 0 - · solidity_simp - subst h - solidity_simp - · solidity_simp - -theorem snapshot_restores_status (s post : ContractState) (amount : Uint256) - (h : (snapshot amount).run s = .success amount post) : - snapshot_spec amount (view s) (view post) := by - unfold snapshot_spec - by_cases h0 : s.msgValue = 0 - · solidity_simp - subst h - solidity_simp - · solidity_simp - -end Contracts.SolidityImportSmoke.Modifiers.Proofs diff --git a/Contracts/SolidityImportSmoke/Modifiers/Spec.lean b/Contracts/SolidityImportSmoke/Modifiers/Spec.lean deleted file mode 100644 index 4b7d370096..0000000000 --- a/Contracts/SolidityImportSmoke/Modifiers/Spec.lean +++ /dev/null @@ -1,36 +0,0 @@ -import Verity.EVM.Uint256 -import Contracts.SolidityImportSmoke.Modifiers.Modifiers - -namespace Contracts.SolidityImportSmoke.Modifiers.Spec - -open Verity -open Verity.EVM.Uint256 -open Contracts.SolidityImportSmoke.Modifiers.Child - -def early_spec (amount : Uint256) (pre post : Storage) : Prop := - post.status = 1 ∧ - post.value = amount ∧ - post.owner = pre.owner ∧ - post.paused = pre.paused - -def guarded_spec (amount : Uint256) (pre post : Storage) : Prop := - post.value = amount ∧ - post.status = pre.status ∧ - post.owner = pre.owner ∧ - post.paused = pre.paused - -def tagged_spec (pre post : Storage) : Prop := - post.helperValue = 1 ∧ - post.value = 0 ∧ - post.status = pre.status ∧ - post.owner = pre.owner ∧ - post.paused = pre.paused - -def snapshot_spec (_amount : Uint256) (pre post : Storage) : Prop := - post.status = pre.status ∧ - post.value = pre.value ∧ - post.owner = pre.owner ∧ - post.paused = pre.paused ∧ - post.helperValue = pre.helperValue - -end Contracts.SolidityImportSmoke.Modifiers.Spec diff --git a/Contracts/SolidityImportSmoke/Modifiers/scripts/modifiers_test.py b/Contracts/SolidityImportSmoke/Modifiers/scripts/modifiers_test.py deleted file mode 100755 index 51f058736a..0000000000 --- a/Contracts/SolidityImportSmoke/Modifiers/scripts/modifiers_test.py +++ /dev/null @@ -1,268 +0,0 @@ -#!/usr/bin/env python3 -"""Acceptance checks for the Solidity importer modifiers slice.""" - -import hashlib -import os -from pathlib import Path -import re -import shutil -import subprocess -import tempfile - -ROOT = Path(__file__).resolve().parents[4] - - -def lake_binary() -> str: - version = (ROOT / "lean-toolchain").read_text().strip().split(":")[-1] - candidates = [ - Path.home() / ".elan/toolchains" / f"leanprover--lean4---{version}" / "bin/lake", - Path("/var/lib/opencode/.elan/toolchains") / f"leanprover--lean4---{version}" / "bin/lake", - ] - for candidate in candidates: - if candidate.is_file(): - return str(candidate) - found = shutil.which("lake") - if found: - return found - raise RuntimeError("lake executable not found") - - -LAKE = lake_binary() -ENV = dict(os.environ) -SMOKE = "Contracts/SolidityImportSmoke/Modifiers" -TARGET = "SolidityImportSmokeModifiers" -NS = "Contracts.SolidityImportSmoke.Modifiers.Child" -PROOF = f"{SMOKE}/Proofs.lean" - - -def check(ok: bool, message: str) -> None: - if not ok: - raise AssertionError(message) - print("PASS " + message, flush=True) - - -def run(root: Path, args: list[str], success: bool = True, contains: str | None = None) -> str: - process = subprocess.run( - args, cwd=root, env=ENV, text=True, capture_output=True, timeout=300 - ) - output = process.stdout + process.stderr - if (process.returncode == 0) != success or (contains and contains not in output) or "PANIC" in output: - raise AssertionError(f"{args}: exit {process.returncode}\n{output}") - return output - - -def main() -> None: - importer_path = ROOT / "Contracts/VaultFromSolidity/Importer/Importer.lean" - syntax_path = ROOT / "Contracts/VaultFromSolidity/Importer/Syntax.lean" - semantics_path = ROOT / "Contracts/VaultFromSolidity/Importer/Semantics.lean" - importer_text = importer_path.read_text() - syntax_text = syntax_path.read_text() - semantics_text = semantics_path.read_text() - check("| seq " in syntax_text and "| block " in syntax_text, - "modifier inlining uses Stmt.seq / Stmt.block") - check("Semantics.lean never sees `_`" in semantics_text or - "this file never sees `_`" in semantics_text, - "modifier placeholder is inlined at parse time") - check("wrapModifiers" in importer_text and "modifier with two placeholders" in importer_text, - "importer inlines modifiers and rejects two placeholders") - check("modifier arguments" in importer_text, - "importer rejects parameterized modifiers") - check("static.visibility == \"private\"" in importer_text and "!static.virtual" in importer_text, - "identifier calls keep private/non-virtual static binding") - check("wrapModifierParts" in importer_text and "inner.shift .uint 0" in importer_text, - "prelude locals are bound over the inlined body and postlude") - - with tempfile.TemporaryDirectory(prefix="verity-mod-check-", dir=ROOT.parent) as directory: - root = Path(directory) - for name in ("Verity", "Compiler", "Contracts", "scripts"): - shutil.copytree(ROOT / name, root / name) - for name in ("lakefile.lean", "lake-manifest.json", "lean-toolchain"): - shutil.copy2(ROOT / name, root / name) - shutil.copytree(ROOT / ".lake/build", root / ".lake/build") - (root / ".lake/solidity-import").mkdir(parents=True) - shutil.copy2(ROOT / ".lake/solidity-import/solc", root / ".lake/solidity-import/solc") - (root / ".lake/packages").symlink_to(ROOT / ".lake/packages", target_is_directory=True) - - source = root / SMOKE / "Modifiers.sol" - original = source.read_bytes() - source_stamp = source.stat() - importer = root / "Contracts/VaultFromSolidity/Importer/Importer.lean" - importer_original = importer.read_bytes() - importer_stamp = importer.stat() - proof = root / PROOF - proof_text = proof.read_text() - theorem_names = re.findall(r"^theorem\s+(\w+)", proof_text, re.M) - - def edit_source(data: bytes) -> None: - source.write_bytes(data) - os.utime(source, ns=(source_stamp.st_atime_ns, source_stamp.st_mtime_ns)) - - def build(success: bool = True, contains: str | None = None) -> str: - return run(root, [LAKE, "build", TARGET], success, contains) - - def artifacts() -> dict[str, tuple[int, str]]: - paths = list((root / ".lake/build/lib/lean/Contracts/SolidityImportSmoke/Modifiers").rglob("*.olean")) - return { - str(path.relative_to(root)): ( - path.stat().st_mtime_ns, - hashlib.sha256(path.read_bytes()).hexdigest(), - ) - for path in paths - } - - def print_names(*names: str) -> str: - probe = root / ".lake/solidity-import/ModPrint.lean" - try: - probe.write_text( - f"import {SMOKE.replace('/', '.')}.Modifiers\n" - + "".join(f"#print {NS}.{name}\n" for name in names) - ) - return run(root, [LAKE, "env", "lean", str(probe)]) - finally: - probe.unlink(missing_ok=True) - - build() - check(True, "baseline lake build SolidityImportSmokeModifiers") - - audit_file = root / ".lake/solidity-import/ModAxiomAudit.lean" - try: - audit_file.write_text( - f"import {SMOKE.replace('/', '.')}.Proofs\n" - + "\n".join( - f"#print axioms {SMOKE.replace('/', '.')}.Proofs.{name}" - for name in theorem_names - ) - + "\n" - ) - audit = run(root, [LAKE, "env", "lean", str(audit_file)]) - finally: - audit_file.unlink(missing_ok=True) - entries = re.findall( - r"'Contracts.SolidityImportSmoke.Modifiers.Proofs.(\w+)' depends on axioms: \[([^\]]*)\]", - audit, - ) - check(set(theorem_names) == {name for name, _ in entries}, - "every modifier theorem appears in #print axioms output") - axioms = {a.strip() for _, values in entries for a in values.split(",") if a.strip()} - check(axioms <= {"propext", "Quot.sound"}, - "no project axioms or sorryAx: " + ", ".join(sorted(axioms))) - - printed_early = print_names("early") - check(".seq" in printed_early and ".block" in printed_early, - "#print Child.early inlines nonReentrant as seq/block") - printed_guarded = print_names("guarded") - check(printed_guarded.find("NotOwner") < printed_guarded.find("EnforcedPause"), - "onlyOwner then whenNotPaused is declaration order") - printed_tagged = print_names("tagged") - check("HelperLike_bump" in printed_tagged and "Child_bump" not in printed_tagged, - "inherited modifier keeps the declaring contract's private helper") - printed_snapshot = print_names("snapshot") - check(".local_" in printed_snapshot and ".block" in printed_snapshot, - "#print Child.snapshot carries restore prelude local into the postlude") - - def broken_theorems(output: str) -> set[str]: - error_lines = [int(value) for value in re.findall(rf"{PROOF}:(\d+):", output)] - starts = [ - (match.group(1), line_no) - for line_no, line in enumerate(proof_text.splitlines(), 1) - if (match := re.match(r"theorem\s+([A-Za-z0-9_']+)", line.strip())) - ] - ranges = { - name: (start, starts[index + 1][1] - 1 if index + 1 < len(starts) - else len(proof_text.splitlines())) - for index, (name, start) in enumerate(starts) - } - return {name for name, (start, end) in ranges.items() - if any(start <= line <= end for line in error_lines)} - - postlude = b" status = 2;\n _;\n status = 1;" - dropped = b" status = 2;\n _;" - check(original.count(postlude) == 1, "nonReentrant postlude has one source target") - edit_source(original.replace(postlude, dropped)) - output = build(False, "Contracts.SolidityImportSmoke.Modifiers.Proofs") - check(bool({"status_restored", "early_meets_spec"} & broken_theorems(output)), - "dropping the nonReentrant postlude breaks status_restored") - edit_source(original) - build() - - swapped = original.replace(b"onlyOwner whenNotPaused", b"whenNotPaused onlyOwner") - check(original != swapped, "modifier-order mutation has a source target") - edit_source(swapped) - output = build(False, "Contracts.SolidityImportSmoke.Modifiers.Proofs") - check("guarded_reverts_not_owner_before_pause" in broken_theorems(output), - "swapping onlyOwner/whenNotPaused breaks guarded_reverts_not_owner_before_pause") - edit_source(original) - build() - - before = artifacts() - importer.write_bytes(importer_original + b"\n-- modifiers identity probe\n") - os.utime(importer, ns=(importer_stamp.st_atime_ns, importer_stamp.st_mtime_ns)) - build() - check(before != artifacts(), "Lean importer content change invalidates modifier artifacts") - importer.write_bytes(importer_original) - build() - - two_ph_old = b" if (msg.sender != owner) revert NotOwner();\n _;" - two_ph_new = b" if (msg.sender != owner) revert NotOwner();\n _;\n _;" - check(original.count(two_ph_old) == 1, "two placeholders mutation has one source target") - edit_source(original.replace(two_ph_old, two_ph_new)) - output = build(False, "modifier with two placeholders") - check(re.search(r"Contracts/SolidityImportSmoke/Modifiers/Modifiers.sol:\d+:\d+:", output) - is not None, - "two placeholders rejected with source position") - edit_source(original) - - args_src = original.replace( - b" modifier onlyOwner() {\n if (msg.sender != owner) revert NotOwner();\n _;\n }", - b" modifier onlyOwner(uint256 unused) {\n unused;\n if (msg.sender != owner) revert NotOwner();\n _;\n }", - ).replace( - b" function guarded(uint256 amount) external onlyOwner whenNotPaused {", - b" function guarded(uint256 amount) external onlyOwner(0) whenNotPaused {", - ) - check(args_src != original, "modifier arguments mutation has a source target") - edit_source(args_src) - output = build(False, "modifier arguments") - check(re.search(r"Contracts/SolidityImportSmoke/Modifiers/Modifiers.sol:\d+:\d+:", output) - is not None, - "modifier arguments rejected with source position") - edit_source(original) - build() - - restore_post = b" uint256 old = status;\n _;\n status = old;" - restore_dropped = b" uint256 old = status;\n _;" - check(original.count(restore_post) == 1, "restore postlude has one source target") - edit_source(original.replace(restore_post, restore_dropped)) - output = build(False, "Contracts.SolidityImportSmoke.Modifiers.Proofs") - check(bool({"snapshot_restores_status"} & broken_theorems(output)), - "dropping the restore postlude breaks snapshot_restores_status") - edit_source(original) - build() - - base_helper = b" function bump() private {\n helperValue = 1;\n }" - base_helper_mut = b" function bump() private {\n helperValue = 9;\n }" - check(original.count(base_helper) == 1, "base private helper has one source target") - edit_source(original.replace(base_helper, base_helper_mut)) - output = build(False, "Contracts.SolidityImportSmoke.Modifiers.Proofs") - check(bool({"tagged_uses_base_helper"} & broken_theorems(output)), - "mutating the declaring contract's private helper breaks tagged_uses_base_helper") - edit_source(original) - build() - poison = b" function poison() private {\n helperValue = 2;\n }" - poison_mut = b" function poison() private {\n helperValue = 9;\n }" - check(original.count(poison) == 1, "unrelated derived private helper has one source target") - edit_source(original.replace(poison, poison_mut)) - build() - mutated_tagged = print_names("tagged") - check("HelperLike_bump" in mutated_tagged and "Child_bump" not in mutated_tagged, - "mutating an unrelated derived private helper does not rebind the inherited modifier") - edit_source(original) - build() - - check(source.read_bytes() == original and importer.read_bytes() == importer_original, - "temporary mutations restored; final baseline passes") - print(f"PASS all modifier-slice acceptance checks ({len(theorem_names)} audited theorems)", - flush=True) - - -if __name__ == "__main__": - main() diff --git a/Contracts/SolidityImportSmoke/Slice.sol b/Contracts/SolidityImportSmoke/Slice.sol new file mode 100644 index 0000000000..63739702fd --- /dev/null +++ b/Contracts/SolidityImportSmoke/Slice.sol @@ -0,0 +1,57 @@ +// SPDX-License-Identifier: MIT +pragma solidity 0.8.34; + +import "./Lib.sol"; + +struct Pos { + uint128 credit; + uint128 pendingFee; + uint128 lastLossFactor; + uint128 lastAccrual; + uint128 debt; + uint128 collateralBitmap; +} + +struct MktState { + uint128 totalUnits; + uint128 lossFactor; +} + +struct Mkt { + uint256 ignored; + uint128[] collateralParams; + uint256 maturity; +} + +contract C { + using L for uint256; + using L for uint128; + + mapping(bytes32 => mapping(address => Pos)) public position; + mapping(bytes32 => MktState) public marketState; + + function f(Mkt memory m, bytes32 id, address user) + external + view + returns (uint128, uint128, uint128) + { + Pos storage p = position[id][user]; + uint128 credit = p.credit; + uint128 lastLoss = p.lastLossFactor; + uint256 post = lastLoss < type(uint128).max + ? credit.mulDivDown(type(uint128).max - marketState[id].lossFactor, type(uint128).max - lastLoss) + : 0; + uint128 pending = p.pendingFee; + uint256 postFee = credit > 0 ? pending - pending.mulDivUp(credit - post, credit) : 0; + uint256 end = L.min(block.timestamp, m.maturity); + uint128 lastAccrual = p.lastAccrual; + uint128 fee = lastAccrual < m.maturity + ? uint128(postFee.mulDivDown(end - lastAccrual, m.maturity - lastAccrual)) + : 0; + return (uint128(post) - fee, uint128(postFee) - fee, fee); + } + + function lossOf(bytes32 market) external view returns (uint128) { + return marketState[market].lossFactor; + } +} diff --git a/Contracts/SolidityImportSmoke/Smoke.lean b/Contracts/SolidityImportSmoke/Smoke.lean new file mode 100644 index 0000000000..64228a7668 --- /dev/null +++ b/Contracts/SolidityImportSmoke/Smoke.lean @@ -0,0 +1,101 @@ +import Compiler.SolidityImport.Import +import Compiler.SolidityImport.Coverage + +open Compiler.CompilationModel +open Compiler.CompilationModel.SolidityImport +open Compiler.CompilationModel.Denote + +solidity_profile smokeBuild where + evmVersion := "osaka" + viaIR := true + optimizerRuns := some 466 + bytecodeHash := "none" + +solidity_import smoke from "Contracts/SolidityImportSmoke" entry "Slice.sol" using smokeBuild + contract C + function f(Mkt, bytes32, address) + +/-- Oracle used only to place the two struct words of this witness. +The numeric claim is about the values `structMember` reads back. -/ +def sliceOracle : DenoteOracle where + mappingSlot base key := base + key + 1 + keccakMemorySlice _ _ _ := 0 + +def packHalves (lo hi : Nat) : Nat := lo + hi * 2 ^ 128 + +/-- `id = 1`, `user = 2`. Position words land at slots 5 and 6; the market +state word lands at slot 3. -/ +def witnessWorld (credit pending lossFactor lastLoss lastAccrual : Nat) : Verity.ContractState := + Verity.defaultState.withStorageWords fun key => + let word := + match key with + | .slot 5 => packHalves credit pending + | .slot 6 => packHalves lastLoss lastAccrual + | .slot 3 => packHalves 0 lossFactor + | _ => 0 + Verity.Core.Uint256.ofNat word + +def witnessBindings (maturity : Nat) : Env := + [("m_maturity", maturity), ("id", 1), ("user", 2)] + +private def smokeBody : List Stmt := + match smoke.model.functions with + | fn :: _ => fn.body + | [] => [] + +def peel (stmts : List Stmt) : Stmt × List Stmt := + match stmts with + | s :: r => (s, r) + | [] => (.panic .arithmeticOverflow, []) + +theorem smokeBody_peel : smokeBody = (peel smokeBody).1 :: (peel smokeBody).2 := by + unfold smokeBody peel + rfl + +def witnessState (credit pending lossFactor lastLoss lastAccrual timestamp maturity : Nat) : DenoteState := + { world := + { witnessWorld credit pending lossFactor lastLoss lastAccrual with + blockTimestamp := Verity.Core.Uint256.ofNat timestamp } + bindings := witnessBindings maturity } + +theorem string_beq_kernel : ("m_maturity" == "id") = false := by + decide + +set_option maxHeartbeats 2000000 in +theorem smoke_step_credit : + execStmt sliceOracle smoke.model.fields (witnessState 100 10 0 0 0 50 100) (peel smokeBody).1 = + .continue + { witnessState 100 10 0 0 0 50 100 with + bindings := bindValue (witnessBindings 100) "credit" 100 } := by + dsimp only [peel, smokeBody, smoke.model] + have hmi : ("m_maturity" == "id") = false := by decide + have hmu : ("m_maturity" == "user") = false := by decide + have hiu : ("id" == "user") = false := by decide + have hft : (false = true) = False := by decide + have h5 : 5 % Verity.Core.Uint256.modulus = 5 := Nat.mod_eq_of_lt (by decide) + have h0m : 0 % Verity.Core.Uint256.modulus = 0 := Nat.mod_eq_of_lt (by decide) + simp only [hmi, hmu, hiu, hft, h5, h0m, Nat.mod_mod, Nat.shiftRight_zero, + execStmt, evalExpr, findFieldWithResolvedSlot_eq_CopyFrom, + findFieldWithResolvedSlotCopyFrom, findStructMembers, findStructMember, readFieldWord, + packedMaskNat, lookupValue, sliceOracle, witnessState, witnessWorld, witnessBindings, + packHalves, bindValue, wordNormalize, Verity.ContractState.withStorageWords, + Verity.ContractState.readSlot, Verity.ContractState.storage, Verity.Core.Uint256.shr, + Verity.Core.Uint256.and, Verity.Core.Uint256.ofNat, bind, Option.bind, Option.map, + Option.getD, List.find?, beq_self_eq_true, ↓reduceIte, + ↓Nat.reduceAdd, ↓Nat.reduceMul, ↓Nat.reducePow, ↓Nat.reduceSub] + have hcredit : + (3402823669209384634633746074317682114660 % Verity.Core.Uint256.modulus &&& + 340282366920938463463374607431768211455 % Verity.Core.Uint256.modulus) % + Verity.Core.Uint256.modulus = 100 := by decide + simp [hcredit] + +-- Interpreter witnesses A/B live in scripts/solidity_import_mutations.py. +-- Keeping test execution out of this proof module satisfies Lean hygiene. + +/-- The generated storage reader follows the imported layout: in the witness, +`position[1][2].credit` is the low half of slot 5 and `pendingFee` its high half. -/ +example : (smoke.position.credit sliceOracle (witnessWorld 100 10 0 0 0) 1 2).val = 100 := by + decide + +example : (smoke.position.pendingFee sliceOracle (witnessWorld 100 10 0 0 0) 1 2).val = 10 := by + decide diff --git a/Contracts/SolidityImportSmoke/Structs/Proofs.lean b/Contracts/SolidityImportSmoke/Structs/Proofs.lean deleted file mode 100644 index 0a77c561df..0000000000 --- a/Contracts/SolidityImportSmoke/Structs/Proofs.lean +++ /dev/null @@ -1,39 +0,0 @@ -import Contracts.SolidityImportSmoke.Structs.Spec -import Verity.Proofs.Stdlib.SolidityImport - -namespace Contracts.SolidityImportSmoke.Structs.Proofs -open Verity -open Verity.Proofs.Stdlib.SolidityImport -open Contracts.SolidityImportSmoke.Structs.Store -open Spec - -theorem set_then_get (s post : ContractState) (amount : Uint256) (who : Address) - (h : (set (amount, who)).run s = .success () post) : - set_spec amount who (view s) (view post) := by - unfold set_spec - cases hbeq : Nat.beq s.msgValue.val 0 - · solidity_simp - · solidity_simp - subst h - solidity_simp - -theorem other_member_unchanged_thm (s post : ContractState) (amount : Uint256) (who : Address) - (h : (set (amount, who)).run s = .success () post) : - other_member_unchanged (amount, who) (view s) (view post) := - (set_then_get s post amount who h).2.2 - -theorem get_meets_spec (s : ContractState) (h0 : s.msgValue = 0) : - ∃ post, get.run s = ContractResult.success ((view s).data_amount, (view s).data_who) post := by - solidity_simp - -theorem make_encodes (s : ContractState) (amount : Uint256) (who : Address) - (h0 : s.msgValue = 0) : - ∃ post, (make amount who).run s = ContractResult.success (amount, who) post := by - solidity_simp - -theorem makeRev_encodes (s : ContractState) (amount : Uint256) (who : Address) - (h0 : s.msgValue = 0) : - ∃ post, (makeRev amount who).run s = ContractResult.success (amount, who) post := by - solidity_simp - -end Contracts.SolidityImportSmoke.Structs.Proofs diff --git a/Contracts/SolidityImportSmoke/Structs/Spec.lean b/Contracts/SolidityImportSmoke/Structs/Spec.lean deleted file mode 100644 index 47883c4fc3..0000000000 --- a/Contracts/SolidityImportSmoke/Structs/Spec.lean +++ /dev/null @@ -1,17 +0,0 @@ -import Verity.EVM.Uint256 -import Contracts.SolidityImportSmoke.Structs.Structs - -namespace Contracts.SolidityImportSmoke.Structs.Spec - -open Verity -open Contracts.SolidityImportSmoke.Structs.Store - -def set_spec (amount : Uint256) (who : Address) (pre post : Storage) : Prop := - post.data_amount = amount ∧ - post.data_who = who ∧ - post.other = pre.other - -def other_member_unchanged (_a : Uint256 × Address) (pre post : Storage) : Prop := - post.other = pre.other - -end Contracts.SolidityImportSmoke.Structs.Spec diff --git a/Contracts/SolidityImportSmoke/Structs/Structs.lean b/Contracts/SolidityImportSmoke/Structs/Structs.lean deleted file mode 100644 index ba4de8e030..0000000000 --- a/Contracts/SolidityImportSmoke/Structs/Structs.lean +++ /dev/null @@ -1,7 +0,0 @@ -import Contracts.VaultFromSolidity.Importer.Importer - -namespace Contracts.SolidityImportSmoke.Structs - -solidity_contract Store from "Structs.sol" - -end Contracts.SolidityImportSmoke.Structs diff --git a/Contracts/SolidityImportSmoke/Structs/Structs.sol b/Contracts/SolidityImportSmoke/Structs/Structs.sol deleted file mode 100644 index 99a6ae6c55..0000000000 --- a/Contracts/SolidityImportSmoke/Structs/Structs.sol +++ /dev/null @@ -1,32 +0,0 @@ -// SPDX-License-Identifier: MIT -pragma solidity ^0.8.33; - -struct Acc { - uint256 amount; - address who; -} - -contract Store { - Acc public data; - uint256 public other; - - function set(Acc memory a) external { - data = a; - } - - function get() external view returns (Acc memory) { - return data; - } - - function setOther(uint256 n) external { - other = n; - } - - function make(uint256 amount, address who) external pure returns (Acc memory) { - return Acc({amount: amount, who: who}); - } - - function makeRev(uint256 amount, address who) external pure returns (Acc memory) { - return Acc({who: who, amount: amount}); - } -} diff --git a/Contracts/SolidityImportSmoke/Structs/scripts/structs_test.py b/Contracts/SolidityImportSmoke/Structs/scripts/structs_test.py deleted file mode 100755 index a11316103c..0000000000 --- a/Contracts/SolidityImportSmoke/Structs/scripts/structs_test.py +++ /dev/null @@ -1,293 +0,0 @@ -#!/usr/bin/env python3 -"""Acceptance checks for the Solidity importer structs slice.""" - -import hashlib -import os -from pathlib import Path -import re -import shutil -import subprocess -import tempfile - -ROOT = Path(__file__).resolve().parents[4] - - -def lake_binary() -> str: - version = (ROOT / "lean-toolchain").read_text().strip().split(":")[-1] - candidates = [ - Path.home() / ".elan/toolchains" / f"leanprover--lean4---{version}" / "bin/lake", - Path("/var/lib/opencode/.elan/toolchains") / f"leanprover--lean4---{version}" / "bin/lake", - ] - for candidate in candidates: - if candidate.is_file(): - return str(candidate) - found = shutil.which("lake") - if found: - return found - raise RuntimeError("lake executable not found") - - -LAKE = lake_binary() -ENV = dict(os.environ) -SMOKE = "Contracts/SolidityImportSmoke/Structs" -TARGET = "SolidityImportSmokeStructs" -NS = "Contracts.SolidityImportSmoke.Structs.Store" -PROOF = f"{SMOKE}/Proofs.lean" - - -def check(ok: bool, message: str) -> None: - if not ok: - raise AssertionError(message) - print("PASS " + message, flush=True) - - -def run(root: Path, args: list[str], success: bool = True, contains: str | None = None) -> str: - process = subprocess.run( - args, cwd=root, env=ENV, text=True, capture_output=True, timeout=300 - ) - output = process.stdout + process.stderr - if (process.returncode == 0) != success or (contains and contains not in output) or "PANIC" in output: - raise AssertionError(f"{args}: exit {process.returncode}\n{output}") - return output - - -def main() -> None: - importer_path = ROOT / "Contracts/VaultFromSolidity/Importer/Importer.lean" - syntax_path = ROOT / "Contracts/VaultFromSolidity/Importer/Syntax.lean" - importer_text = importer_path.read_text() - syntax_text = syntax_path.read_text() - check("| pair " in syntax_text and "| fst " in syntax_text and "| snd " in syntax_text, - "user structs are Expr.pair / fst / snd") - check("StructDefinition" in importer_text and "UserDefinedTypeName" in importer_text, - "importer accepts struct AST nodes") - check("structConstructorCall" in importer_text, - "importer encodes struct constructors") - check("| pairRev " in syntax_text and ".pairRev " in importer_text, - "named Acc({who, amount}) uses pairRev after source-order eval") - check(".pairRev a b =>" in (ROOT / "Contracts/VaultFromSolidity/Importer/Semantics.lean").read_text(), - "pairRev evaluates who then amount and packs uint-then-addr") - check("struct with a dynamic member" in importer_text and "mapping with struct key" in importer_text, - "importer rejects dynamic members and struct mapping keys") - - with tempfile.TemporaryDirectory(prefix="verity-struct-check-", dir=ROOT.parent) as directory: - root = Path(directory) - for name in ("Verity", "Compiler", "Contracts", "scripts"): - shutil.copytree(ROOT / name, root / name) - for name in ("lakefile.lean", "lake-manifest.json", "lean-toolchain"): - shutil.copy2(ROOT / name, root / name) - shutil.copytree(ROOT / ".lake/build", root / ".lake/build") - (root / ".lake/solidity-import").mkdir(parents=True) - shutil.copy2(ROOT / ".lake/solidity-import/solc", root / ".lake/solidity-import/solc") - (root / ".lake/packages").symlink_to(ROOT / ".lake/packages", target_is_directory=True) - - source = root / SMOKE / "Structs.sol" - original = source.read_bytes() - source_stamp = source.stat() - importer = root / "Contracts/VaultFromSolidity/Importer/Importer.lean" - importer_original = importer.read_bytes() - importer_stamp = importer.stat() - proof = root / PROOF - proof_text = proof.read_text() - theorem_names = re.findall(r"^theorem\s+(\w+)", proof_text, re.M) - - def edit_source(data: bytes) -> None: - source.write_bytes(data) - os.utime(source, ns=(source_stamp.st_atime_ns, source_stamp.st_mtime_ns)) - - def build(success: bool = True, contains: str | None = None) -> str: - return run(root, [LAKE, "build", TARGET], success, contains) - - def artifacts() -> dict[str, tuple[int, str]]: - paths = list((root / ".lake/build/lib/lean/Contracts/SolidityImportSmoke/Structs").rglob("*.olean")) - return { - str(path.relative_to(root)): ( - path.stat().st_mtime_ns, - hashlib.sha256(path.read_bytes()).hexdigest(), - ) - for path in paths - } - - def print_names(*names: str) -> str: - probe = root / ".lake/solidity-import/StructPrint.lean" - try: - probe.write_text( - f"import {SMOKE.replace('/', '.')}.Structs\n" - + "".join(f"#print {NS}.{name}\n" for name in names) - ) - return run(root, [LAKE, "env", "lean", str(probe)]) - finally: - probe.unlink(missing_ok=True) - - def slot_numbers() -> str: - slot_probe = root / ".lake/solidity-import/StructSlotProbe.lean" - try: - slot_probe.write_text( - f"import {SMOKE.replace('/', '.')}.Structs\n" - f"#eval [{NS}.data_amountSlot.slot, {NS}.data_whoSlot.slot, {NS}.otherSlot.slot]\n" - ) - found = re.findall(r"\[\d+(?:, \d+)*\]", run(root, [LAKE, "env", "lean", str(slot_probe)])) - if not found: - raise AssertionError("slot probe produced no list") - return found[0] - finally: - slot_probe.unlink(missing_ok=True) - - build() - check(True, "baseline lake build SolidityImportSmokeStructs") - - audit_file = root / ".lake/solidity-import/StructAxiomAudit.lean" - try: - audit_file.write_text( - f"import {SMOKE.replace('/', '.')}.Proofs\n" - + "\n".join( - f"#print axioms {SMOKE.replace('/', '.')}.Proofs.{name}" - for name in theorem_names - ) - + "\n" - ) - audit = run(root, [LAKE, "env", "lean", str(audit_file)]) - finally: - audit_file.unlink(missing_ok=True) - entries = re.findall( - r"'Contracts.SolidityImportSmoke.Structs.Proofs.(\w+)' depends on axioms: \[([^\]]*)\]", - audit, - ) - check(set(theorem_names) == {name for name, _ in entries}, - "every struct theorem appears in #print axioms output") - axioms = {a.strip() for _, values in entries for a in values.split(",") if a.strip()} - check(axioms <= {"propext", "Quot.sound"}, - "no project axioms or sorryAx: " + ", ".join(sorted(axioms))) - - printed = print_names("set", "get", "make", "makeRev") - check(".fst" in printed and ".snd" in printed, - "#print Store.set decodes struct members") - check(".pair" in printed, - "#print Store.make / Store.get encode the user struct") - check(".pairRev" in printed, - "#print Store.makeRev encodes named Acc({who, amount})") - check(slot_numbers() == "[0, 1, 2]", - "struct members occupy consecutive solc slots [amount, who, other]") - - def broken_theorems(output: str) -> set[str]: - error_lines = [int(value) for value in re.findall(rf"{PROOF}:(\d+):", output)] - starts = [ - (match.group(1), line_no) - for line_no, line in enumerate(proof_text.splitlines(), 1) - if (match := re.match(r"theorem\s+([A-Za-z0-9_']+)", line.strip())) - ] - ranges = { - name: (start, starts[index + 1][1] - 1 if index + 1 < len(starts) - else len(proof_text.splitlines())) - for index, (name, start) in enumerate(starts) - } - return {name for name, (start, end) in ranges.items() - if any(start <= line <= end for line in error_lines)} - - dropped_who = original.replace( - b" data = a;\n", - b" data = Acc({amount: a.amount, who: data.who});\n", - ) - check(original != dropped_who, "dropped who write has a source target") - edit_source(dropped_who) - output = build(False, "Contracts.SolidityImportSmoke.Structs.Proofs") - check("set_then_get" in broken_theorems(output), - "dropping the who write breaks set_then_get") - edit_source(original) - build() - - reordered = original.replace( - b"struct Acc {\n uint256 amount;\n address who;\n}", - b"struct Acc {\n address who;\n uint256 amount;\n}", - ) - check(original != reordered, "struct member reorder has a source target") - edit_source(reordered) - output = build(False, "unsupported struct shape") - check(re.search(r"Contracts/SolidityImportSmoke/Structs/Structs.sol:\d+:\d+:", output) - is not None, - "reordering pair members is rejected with a source position") - edit_source(original) - build() - - named_reorder = original.replace( - b" return Acc({amount: amount, who: who});", - b" return Acc({who: who, amount: amount});", - ) - check(original != named_reorder, "named constructor reorder has a source target") - edit_source(named_reorder) - build() - printed_rev = print_names("make") - check(".pairRev" in printed_rev, - "named Acc({who, amount}) imports as pairRev") - check(True, "named Acc({who: who, amount: amount}) still imports") - edit_source(original) - build() - printed_fwd = print_names("make") - check(".pair" in printed_fwd and ".pairRev" not in printed_fwd, - "named Acc({amount, who}) stays Expr.pair") - - positional = original.replace( - b" return Acc({amount: amount, who: who});", - b" return Acc(amount, who);", - ) - check(original != positional, "positional constructor has a source target") - edit_source(positional) - build() - printed_pos = print_names("make") - check(".pair" in printed_pos and ".pairRev" not in printed_pos, - "positional Acc(amount, who) stays AST/member order as Expr.pair") - edit_source(original) - build() - - before = artifacts() - importer.write_bytes(importer_original + b"\n-- structs identity probe\n") - os.utime(importer, ns=(importer_stamp.st_atime_ns, importer_stamp.st_mtime_ns)) - build() - check(before != artifacts(), "Lean importer content change invalidates struct artifacts") - importer.write_bytes(importer_original) - build() - - dyn_src = original.replace( - b" address who;\n", - b" address who;\n string note;\n", - ).replace( - b" return Acc({amount: amount, who: who});", - b" return Acc({amount: amount, who: who, note: \"\"});", - ).replace( - b" return Acc({who: who, amount: amount});", - b" return Acc({who: who, amount: amount, note: \"\"});", - ) - check(dyn_src != original, "dynamic member mutation has a source target") - edit_source(dyn_src) - output = build(False) - check( - "struct with a dynamic member" in output or "unsupported struct shape" in output, - "dynamic member is rejected", - ) - check(re.search(r"Contracts/SolidityImportSmoke/Structs/Structs.sol:\d+:\d+:", output) - is not None, - "dynamic member rejected with source position") - edit_source(original) - - key_old = b" Acc public data;\n uint256 public other;\n" - key_new = b" Acc public data;\n uint256 public other;\n mapping(Acc => uint256) private keyed;\n" - check(original.count(key_old) == 1, "struct mapping key mutation has one source target") - edit_source(original.replace(key_old, key_new)) - output = build(False) - check( - "mapping with struct key" in output or "TypeError" in output, - "mapping with struct key is rejected", - ) - check(re.search(r"Contracts/SolidityImportSmoke/Structs/Structs.sol:\d+:\d+:", output) - is not None, - "mapping with struct key rejected with source position") - edit_source(original) - build() - - check(source.read_bytes() == original and importer.read_bytes() == importer_original, - "temporary mutations restored; final baseline passes") - print(f"PASS all struct-slice acceptance checks ({len(theorem_names)} audited theorems)", - flush=True) - - -if __name__ == "__main__": - main() diff --git a/Contracts/SolidityImportSmoke/differential-cases.json b/Contracts/SolidityImportSmoke/differential-cases.json new file mode 100644 index 0000000000..294a23b15e --- /dev/null +++ b/Contracts/SolidityImportSmoke/differential-cases.json @@ -0,0 +1,4 @@ +[ + {"name":"success", "credit":"100", "pending":"10", "lastLoss":"0", "lossFactor":"0", "lastAccrual":"0", "timestamp":"50", "maturity":"100", "id":"1", "user":"2"}, + {"name":"revert", "credit":"1", "pending":"2", "lastLoss":"0", "lossFactor":"0", "lastAccrual":"0", "timestamp":"1", "maturity":"1", "id":"1", "user":"2"} +] diff --git a/Contracts/SolidityImportSmoke/differential-loss.json b/Contracts/SolidityImportSmoke/differential-loss.json new file mode 100644 index 0000000000..b9a5790772 --- /dev/null +++ b/Contracts/SolidityImportSmoke/differential-loss.json @@ -0,0 +1,27 @@ +{ + "project": ".", + "entry": "Slice.sol", + "contract": "C", + "function": "lossOf", + "param_types": [ + "bytes32" + ], + "variables": { + "market": 256, + "lossFactor": 128 + }, + "arguments": { + "market": "market" + }, + "storage": [ + { + "field": "marketState", + "keys": [ + "market" + ], + "members": { + "lossFactor": "lossFactor" + } + } + ] +} diff --git a/Contracts/SolidityImportSmoke/differential.json b/Contracts/SolidityImportSmoke/differential.json new file mode 100644 index 0000000000..64f83e3d42 --- /dev/null +++ b/Contracts/SolidityImportSmoke/differential.json @@ -0,0 +1,63 @@ +{ + "project": ".", + "entry": "Slice.sol", + "contract": "C", + "function": "f", + "param_types": [ + "struct Mkt", + "bytes32", + "address" + ], + "variables": { + "maturity": 256, + "id": 256, + "user": 160, + "credit": 128, + "pending": 128, + "lastLoss": 128, + "lastAccrual": 128, + "lossFactor": 128, + "timestamp": 256 + }, + "arguments": { + "m.maturity": "maturity", + "id": "id", + "user": "user" + }, + "timestamp": "timestamp", + "ordered_pairs": [ + [ + "lastLoss", + "lossFactor" + ], + [ + "lastAccrual", + "timestamp" + ] + ], + "storage": [ + { + "field": "position", + "keys": [ + "id", + "user" + ], + "members": { + "credit": "credit", + "pendingFee": "pending", + "lastLossFactor": "lastLoss", + "lastAccrual": "lastAccrual" + } + }, + { + "field": "marketState", + "keys": [ + "id" + ], + "members": { + "lossFactor": "lossFactor" + } + } + ], + "corpus": "differential-cases.json" +} diff --git a/Contracts/SolidityImportSmoke/model.golden b/Contracts/SolidityImportSmoke/model.golden new file mode 100644 index 0000000000..9f3a3471a6 --- /dev/null +++ b/Contracts/SolidityImportSmoke/model.golden @@ -0,0 +1,497 @@ +{ name := "C", + contractId := 1, + fields := [{ name := "position", + ty := Compiler.CompilationModel.FieldType.mappingStruct2 + (Compiler.CompilationModel.MappingKeyType.bytes32) + (Compiler.CompilationModel.MappingKeyType.address) + [{ name := "credit", + ty := Compiler.CompilationModel.StructMemberType.uint256, + wordOffset := 0, + packed := some { offset := 0, width := 128 } }, + { name := "pendingFee", + ty := Compiler.CompilationModel.StructMemberType.uint256, + wordOffset := 0, + packed := some { offset := 128, width := 128 } }, + { name := "lastLossFactor", + ty := Compiler.CompilationModel.StructMemberType.uint256, + wordOffset := 1, + packed := some { offset := 0, width := 128 } }, + { name := "lastAccrual", + ty := Compiler.CompilationModel.StructMemberType.uint256, + wordOffset := 1, + packed := some { offset := 128, width := 128 } }, + { name := "debt", + ty := Compiler.CompilationModel.StructMemberType.uint256, + wordOffset := 2, + packed := some { offset := 0, width := 128 } }, + { name := "collateralBitmap", + ty := Compiler.CompilationModel.StructMemberType.uint256, + wordOffset := 2, + packed := some { offset := 128, width := 128 } }], + isTransient := false, + slot := some 0, + packedBits := none, + aliasSlots := [] }, + { name := "marketState", + ty := Compiler.CompilationModel.FieldType.mappingStruct + (Compiler.CompilationModel.MappingKeyType.bytes32) + [{ name := "totalUnits", + ty := Compiler.CompilationModel.StructMemberType.uint256, + wordOffset := 0, + packed := some { offset := 0, width := 128 } }, + { name := "lossFactor", + ty := Compiler.CompilationModel.StructMemberType.uint256, + wordOffset := 0, + packed := some { offset := 128, width := 128 } }], + isTransient := false, + slot := some 1, + packedBits := none, + aliasSlots := [] }], + immutables := [], + roles := [], + reservedSlotRanges := [], + slotAliasRanges := [], + constructor := none, + functions := [{ name := "f", + params := [{ name := "m_maturity", ty := Compiler.CompilationModel.ParamType.uint256 }, + { name := "id", ty := Compiler.CompilationModel.ParamType.bytes32 }, + { name := "user", ty := Compiler.CompilationModel.ParamType.address }], + returnType := none, + returns := [Compiler.CompilationModel.ParamType.uintN 128, + Compiler.CompilationModel.ParamType.uintN 128, + Compiler.CompilationModel.ParamType.uintN 128], + isPayable := false, + isView := true, + isPure := false, + body := [Compiler.CompilationModel.Stmt.letVar + "credit" + (Compiler.CompilationModel.Expr.structMember2 + "position" + (Compiler.CompilationModel.Expr.param "id") + (Compiler.CompilationModel.Expr.param "user") + "credit"), + Compiler.CompilationModel.Stmt.letVar + "lastLoss" + (Compiler.CompilationModel.Expr.structMember2 + "position" + (Compiler.CompilationModel.Expr.param "id") + (Compiler.CompilationModel.Expr.param "user") + "lastLossFactor"), + Compiler.CompilationModel.Stmt.letVar + "_verity_slice_tmp_0" + (Compiler.CompilationModel.Expr.lt + (Compiler.CompilationModel.Expr.localVar "lastLoss") + (Compiler.CompilationModel.Expr.literal 340282366920938463463374607431768211455)), + Compiler.CompilationModel.Stmt.letVar + "_verity_slice_tmp_9" + (Compiler.CompilationModel.Expr.literal 0), + Compiler.CompilationModel.Stmt.ite + (Compiler.CompilationModel.Expr.localVar "_verity_slice_tmp_0") + [Compiler.CompilationModel.Stmt.letVar + "_verity_slice_tmp_1" + (Compiler.CompilationModel.Expr.structMember + "marketState" + (Compiler.CompilationModel.Expr.param "id") + "lossFactor"), + Compiler.CompilationModel.Stmt.letVar + "_verity_slice_tmp_2" + (Compiler.CompilationModel.Expr.literal 0), + Compiler.CompilationModel.Stmt.ite + (Compiler.CompilationModel.Expr.lt + (Compiler.CompilationModel.Expr.literal 340282366920938463463374607431768211455) + (Compiler.CompilationModel.Expr.localVar "_verity_slice_tmp_1")) + [Compiler.CompilationModel.Stmt.panic (Verity.Core.PanicCode.arithmeticOverflow)] + [Compiler.CompilationModel.Stmt.assignVar + "_verity_slice_tmp_2" + (Compiler.CompilationModel.Expr.sub + (Compiler.CompilationModel.Expr.literal 340282366920938463463374607431768211455) + (Compiler.CompilationModel.Expr.localVar "_verity_slice_tmp_1"))], + Compiler.CompilationModel.Stmt.letVar + "_verity_slice_tmp_4" + (Compiler.CompilationModel.Expr.localVar "_verity_slice_tmp_2"), + Compiler.CompilationModel.Stmt.letVar + "_verity_slice_tmp_3" + (Compiler.CompilationModel.Expr.literal 0), + Compiler.CompilationModel.Stmt.ite + (Compiler.CompilationModel.Expr.lt + (Compiler.CompilationModel.Expr.literal 340282366920938463463374607431768211455) + (Compiler.CompilationModel.Expr.localVar "lastLoss")) + [Compiler.CompilationModel.Stmt.panic (Verity.Core.PanicCode.arithmeticOverflow)] + [Compiler.CompilationModel.Stmt.assignVar + "_verity_slice_tmp_3" + (Compiler.CompilationModel.Expr.sub + (Compiler.CompilationModel.Expr.literal 340282366920938463463374607431768211455) + (Compiler.CompilationModel.Expr.localVar "lastLoss"))], + Compiler.CompilationModel.Stmt.letVar + "_verity_slice_tmp_5" + (Compiler.CompilationModel.Expr.localVar "_verity_slice_tmp_3"), + Compiler.CompilationModel.Stmt.letVar + "_verity_slice_tmp_6" + (Compiler.CompilationModel.Expr.literal 0), + Compiler.CompilationModel.Stmt.ite + (Compiler.CompilationModel.Expr.eq + (Compiler.CompilationModel.Expr.localVar "credit") + (Compiler.CompilationModel.Expr.literal 0)) + [Compiler.CompilationModel.Stmt.assignVar + "_verity_slice_tmp_6" + (Compiler.CompilationModel.Expr.mul + (Compiler.CompilationModel.Expr.localVar "credit") + (Compiler.CompilationModel.Expr.localVar "_verity_slice_tmp_4"))] + [Compiler.CompilationModel.Stmt.ite + (Compiler.CompilationModel.Expr.eq + (Compiler.CompilationModel.Expr.div + (Compiler.CompilationModel.Expr.mul + (Compiler.CompilationModel.Expr.localVar "credit") + (Compiler.CompilationModel.Expr.localVar "_verity_slice_tmp_4")) + (Compiler.CompilationModel.Expr.localVar "credit")) + (Compiler.CompilationModel.Expr.localVar "_verity_slice_tmp_4")) + [Compiler.CompilationModel.Stmt.assignVar + "_verity_slice_tmp_6" + (Compiler.CompilationModel.Expr.mul + (Compiler.CompilationModel.Expr.localVar "credit") + (Compiler.CompilationModel.Expr.localVar "_verity_slice_tmp_4"))] + [Compiler.CompilationModel.Stmt.panic (Verity.Core.PanicCode.arithmeticOverflow)]], + Compiler.CompilationModel.Stmt.letVar + "_verity_slice_tmp_7" + (Compiler.CompilationModel.Expr.localVar "_verity_slice_tmp_6"), + Compiler.CompilationModel.Stmt.letVar + "_verity_slice_tmp_8" + (Compiler.CompilationModel.Expr.literal 0), + Compiler.CompilationModel.Stmt.ite + (Compiler.CompilationModel.Expr.eq + (Compiler.CompilationModel.Expr.localVar "_verity_slice_tmp_5") + (Compiler.CompilationModel.Expr.literal 0)) + [Compiler.CompilationModel.Stmt.panic (Verity.Core.PanicCode.divisionByZero)] + [Compiler.CompilationModel.Stmt.assignVar + "_verity_slice_tmp_8" + (Compiler.CompilationModel.Expr.div + (Compiler.CompilationModel.Expr.localVar "_verity_slice_tmp_7") + (Compiler.CompilationModel.Expr.localVar "_verity_slice_tmp_5"))], + Compiler.CompilationModel.Stmt.assignVar + "_verity_slice_tmp_9" + (Compiler.CompilationModel.Expr.localVar "_verity_slice_tmp_8")] + [Compiler.CompilationModel.Stmt.assignVar + "_verity_slice_tmp_9" + (Compiler.CompilationModel.Expr.literal 0)], + Compiler.CompilationModel.Stmt.letVar + "post" + (Compiler.CompilationModel.Expr.localVar "_verity_slice_tmp_9"), + Compiler.CompilationModel.Stmt.letVar + "pending" + (Compiler.CompilationModel.Expr.structMember2 + "position" + (Compiler.CompilationModel.Expr.param "id") + (Compiler.CompilationModel.Expr.param "user") + "pendingFee"), + Compiler.CompilationModel.Stmt.letVar + "_verity_slice_tmp_10" + (Compiler.CompilationModel.Expr.gt + (Compiler.CompilationModel.Expr.localVar "credit") + (Compiler.CompilationModel.Expr.literal 0)), + Compiler.CompilationModel.Stmt.letVar + "_verity_slice_tmp_22" + (Compiler.CompilationModel.Expr.literal 0), + Compiler.CompilationModel.Stmt.ite + (Compiler.CompilationModel.Expr.localVar "_verity_slice_tmp_10") + [Compiler.CompilationModel.Stmt.letVar + "_verity_slice_tmp_11" + (Compiler.CompilationModel.Expr.literal 0), + Compiler.CompilationModel.Stmt.ite + (Compiler.CompilationModel.Expr.lt + (Compiler.CompilationModel.Expr.localVar "credit") + (Compiler.CompilationModel.Expr.localVar "post")) + [Compiler.CompilationModel.Stmt.panic (Verity.Core.PanicCode.arithmeticOverflow)] + [Compiler.CompilationModel.Stmt.assignVar + "_verity_slice_tmp_11" + (Compiler.CompilationModel.Expr.sub + (Compiler.CompilationModel.Expr.localVar "credit") + (Compiler.CompilationModel.Expr.localVar "post"))], + Compiler.CompilationModel.Stmt.letVar + "_verity_slice_tmp_12" + (Compiler.CompilationModel.Expr.localVar "_verity_slice_tmp_11"), + Compiler.CompilationModel.Stmt.letVar + "_verity_slice_tmp_13" + (Compiler.CompilationModel.Expr.literal 0), + Compiler.CompilationModel.Stmt.ite + (Compiler.CompilationModel.Expr.eq + (Compiler.CompilationModel.Expr.localVar "pending") + (Compiler.CompilationModel.Expr.literal 0)) + [Compiler.CompilationModel.Stmt.assignVar + "_verity_slice_tmp_13" + (Compiler.CompilationModel.Expr.mul + (Compiler.CompilationModel.Expr.localVar "pending") + (Compiler.CompilationModel.Expr.localVar "_verity_slice_tmp_12"))] + [Compiler.CompilationModel.Stmt.ite + (Compiler.CompilationModel.Expr.eq + (Compiler.CompilationModel.Expr.div + (Compiler.CompilationModel.Expr.mul + (Compiler.CompilationModel.Expr.localVar "pending") + (Compiler.CompilationModel.Expr.localVar "_verity_slice_tmp_12")) + (Compiler.CompilationModel.Expr.localVar "pending")) + (Compiler.CompilationModel.Expr.localVar "_verity_slice_tmp_12")) + [Compiler.CompilationModel.Stmt.assignVar + "_verity_slice_tmp_13" + (Compiler.CompilationModel.Expr.mul + (Compiler.CompilationModel.Expr.localVar "pending") + (Compiler.CompilationModel.Expr.localVar "_verity_slice_tmp_12"))] + [Compiler.CompilationModel.Stmt.panic (Verity.Core.PanicCode.arithmeticOverflow)]], + Compiler.CompilationModel.Stmt.letVar + "_verity_slice_tmp_15" + (Compiler.CompilationModel.Expr.localVar "_verity_slice_tmp_13"), + Compiler.CompilationModel.Stmt.letVar + "_verity_slice_tmp_14" + (Compiler.CompilationModel.Expr.literal 0), + Compiler.CompilationModel.Stmt.ite + (Compiler.CompilationModel.Expr.lt + (Compiler.CompilationModel.Expr.localVar "credit") + (Compiler.CompilationModel.Expr.literal 1)) + [Compiler.CompilationModel.Stmt.panic (Verity.Core.PanicCode.arithmeticOverflow)] + [Compiler.CompilationModel.Stmt.assignVar + "_verity_slice_tmp_14" + (Compiler.CompilationModel.Expr.sub + (Compiler.CompilationModel.Expr.localVar "credit") + (Compiler.CompilationModel.Expr.literal 1))], + Compiler.CompilationModel.Stmt.letVar + "_verity_slice_tmp_16" + (Compiler.CompilationModel.Expr.localVar "_verity_slice_tmp_14"), + Compiler.CompilationModel.Stmt.letVar + "_verity_slice_tmp_17" + (Compiler.CompilationModel.Expr.literal 0), + Compiler.CompilationModel.Stmt.ite + (Compiler.CompilationModel.Expr.lt + (Compiler.CompilationModel.Expr.add + (Compiler.CompilationModel.Expr.localVar "_verity_slice_tmp_15") + (Compiler.CompilationModel.Expr.localVar "_verity_slice_tmp_16")) + (Compiler.CompilationModel.Expr.localVar "_verity_slice_tmp_15")) + [Compiler.CompilationModel.Stmt.panic (Verity.Core.PanicCode.arithmeticOverflow)] + [Compiler.CompilationModel.Stmt.assignVar + "_verity_slice_tmp_17" + (Compiler.CompilationModel.Expr.add + (Compiler.CompilationModel.Expr.localVar "_verity_slice_tmp_15") + (Compiler.CompilationModel.Expr.localVar "_verity_slice_tmp_16"))], + Compiler.CompilationModel.Stmt.letVar + "_verity_slice_tmp_18" + (Compiler.CompilationModel.Expr.localVar "_verity_slice_tmp_17"), + Compiler.CompilationModel.Stmt.letVar + "_verity_slice_tmp_19" + (Compiler.CompilationModel.Expr.literal 0), + Compiler.CompilationModel.Stmt.ite + (Compiler.CompilationModel.Expr.eq + (Compiler.CompilationModel.Expr.localVar "credit") + (Compiler.CompilationModel.Expr.literal 0)) + [Compiler.CompilationModel.Stmt.panic (Verity.Core.PanicCode.divisionByZero)] + [Compiler.CompilationModel.Stmt.assignVar + "_verity_slice_tmp_19" + (Compiler.CompilationModel.Expr.div + (Compiler.CompilationModel.Expr.localVar "_verity_slice_tmp_18") + (Compiler.CompilationModel.Expr.localVar "credit"))], + Compiler.CompilationModel.Stmt.letVar + "_verity_slice_tmp_20" + (Compiler.CompilationModel.Expr.localVar "_verity_slice_tmp_19"), + Compiler.CompilationModel.Stmt.letVar + "_verity_slice_tmp_21" + (Compiler.CompilationModel.Expr.literal 0), + Compiler.CompilationModel.Stmt.ite + (Compiler.CompilationModel.Expr.lt + (Compiler.CompilationModel.Expr.localVar "pending") + (Compiler.CompilationModel.Expr.localVar "_verity_slice_tmp_20")) + [Compiler.CompilationModel.Stmt.panic (Verity.Core.PanicCode.arithmeticOverflow)] + [Compiler.CompilationModel.Stmt.assignVar + "_verity_slice_tmp_21" + (Compiler.CompilationModel.Expr.sub + (Compiler.CompilationModel.Expr.localVar "pending") + (Compiler.CompilationModel.Expr.localVar "_verity_slice_tmp_20"))], + Compiler.CompilationModel.Stmt.assignVar + "_verity_slice_tmp_22" + (Compiler.CompilationModel.Expr.localVar "_verity_slice_tmp_21")] + [Compiler.CompilationModel.Stmt.assignVar + "_verity_slice_tmp_22" + (Compiler.CompilationModel.Expr.literal 0)], + Compiler.CompilationModel.Stmt.letVar + "postFee" + (Compiler.CompilationModel.Expr.localVar "_verity_slice_tmp_22"), + Compiler.CompilationModel.Stmt.letVar + "end" + (Compiler.CompilationModel.Expr.bitXor + (Compiler.CompilationModel.Expr.blockTimestamp) + (Compiler.CompilationModel.Expr.mul + (Compiler.CompilationModel.Expr.bitXor + (Compiler.CompilationModel.Expr.blockTimestamp) + (Compiler.CompilationModel.Expr.param "m_maturity")) + (Compiler.CompilationModel.Expr.lt + (Compiler.CompilationModel.Expr.param "m_maturity") + (Compiler.CompilationModel.Expr.blockTimestamp)))), + Compiler.CompilationModel.Stmt.letVar + "lastAccrual" + (Compiler.CompilationModel.Expr.structMember2 + "position" + (Compiler.CompilationModel.Expr.param "id") + (Compiler.CompilationModel.Expr.param "user") + "lastAccrual"), + Compiler.CompilationModel.Stmt.letVar + "_verity_slice_tmp_23" + (Compiler.CompilationModel.Expr.lt + (Compiler.CompilationModel.Expr.localVar "lastAccrual") + (Compiler.CompilationModel.Expr.param "m_maturity")), + Compiler.CompilationModel.Stmt.letVar + "_verity_slice_tmp_33" + (Compiler.CompilationModel.Expr.literal 0), + Compiler.CompilationModel.Stmt.ite + (Compiler.CompilationModel.Expr.localVar "_verity_slice_tmp_23") + [Compiler.CompilationModel.Stmt.letVar + "_verity_slice_tmp_24" + (Compiler.CompilationModel.Expr.literal 0), + Compiler.CompilationModel.Stmt.ite + (Compiler.CompilationModel.Expr.lt + (Compiler.CompilationModel.Expr.localVar "end") + (Compiler.CompilationModel.Expr.localVar "lastAccrual")) + [Compiler.CompilationModel.Stmt.panic (Verity.Core.PanicCode.arithmeticOverflow)] + [Compiler.CompilationModel.Stmt.assignVar + "_verity_slice_tmp_24" + (Compiler.CompilationModel.Expr.sub + (Compiler.CompilationModel.Expr.localVar "end") + (Compiler.CompilationModel.Expr.localVar "lastAccrual"))], + Compiler.CompilationModel.Stmt.letVar + "_verity_slice_tmp_26" + (Compiler.CompilationModel.Expr.localVar "_verity_slice_tmp_24"), + Compiler.CompilationModel.Stmt.letVar + "_verity_slice_tmp_25" + (Compiler.CompilationModel.Expr.literal 0), + Compiler.CompilationModel.Stmt.ite + (Compiler.CompilationModel.Expr.lt + (Compiler.CompilationModel.Expr.param "m_maturity") + (Compiler.CompilationModel.Expr.localVar "lastAccrual")) + [Compiler.CompilationModel.Stmt.panic (Verity.Core.PanicCode.arithmeticOverflow)] + [Compiler.CompilationModel.Stmt.assignVar + "_verity_slice_tmp_25" + (Compiler.CompilationModel.Expr.sub + (Compiler.CompilationModel.Expr.param "m_maturity") + (Compiler.CompilationModel.Expr.localVar "lastAccrual"))], + Compiler.CompilationModel.Stmt.letVar + "_verity_slice_tmp_27" + (Compiler.CompilationModel.Expr.localVar "_verity_slice_tmp_25"), + Compiler.CompilationModel.Stmt.letVar + "_verity_slice_tmp_28" + (Compiler.CompilationModel.Expr.literal 0), + Compiler.CompilationModel.Stmt.ite + (Compiler.CompilationModel.Expr.eq + (Compiler.CompilationModel.Expr.localVar "postFee") + (Compiler.CompilationModel.Expr.literal 0)) + [Compiler.CompilationModel.Stmt.assignVar + "_verity_slice_tmp_28" + (Compiler.CompilationModel.Expr.mul + (Compiler.CompilationModel.Expr.localVar "postFee") + (Compiler.CompilationModel.Expr.localVar "_verity_slice_tmp_26"))] + [Compiler.CompilationModel.Stmt.ite + (Compiler.CompilationModel.Expr.eq + (Compiler.CompilationModel.Expr.div + (Compiler.CompilationModel.Expr.mul + (Compiler.CompilationModel.Expr.localVar "postFee") + (Compiler.CompilationModel.Expr.localVar "_verity_slice_tmp_26")) + (Compiler.CompilationModel.Expr.localVar "postFee")) + (Compiler.CompilationModel.Expr.localVar "_verity_slice_tmp_26")) + [Compiler.CompilationModel.Stmt.assignVar + "_verity_slice_tmp_28" + (Compiler.CompilationModel.Expr.mul + (Compiler.CompilationModel.Expr.localVar "postFee") + (Compiler.CompilationModel.Expr.localVar "_verity_slice_tmp_26"))] + [Compiler.CompilationModel.Stmt.panic (Verity.Core.PanicCode.arithmeticOverflow)]], + Compiler.CompilationModel.Stmt.letVar + "_verity_slice_tmp_29" + (Compiler.CompilationModel.Expr.localVar "_verity_slice_tmp_28"), + Compiler.CompilationModel.Stmt.letVar + "_verity_slice_tmp_30" + (Compiler.CompilationModel.Expr.literal 0), + Compiler.CompilationModel.Stmt.ite + (Compiler.CompilationModel.Expr.eq + (Compiler.CompilationModel.Expr.localVar "_verity_slice_tmp_27") + (Compiler.CompilationModel.Expr.literal 0)) + [Compiler.CompilationModel.Stmt.panic (Verity.Core.PanicCode.divisionByZero)] + [Compiler.CompilationModel.Stmt.assignVar + "_verity_slice_tmp_30" + (Compiler.CompilationModel.Expr.div + (Compiler.CompilationModel.Expr.localVar "_verity_slice_tmp_29") + (Compiler.CompilationModel.Expr.localVar "_verity_slice_tmp_27"))], + Compiler.CompilationModel.Stmt.letVar + "_verity_slice_tmp_31" + (Compiler.CompilationModel.Expr.localVar "_verity_slice_tmp_30"), + Compiler.CompilationModel.Stmt.letVar + "_verity_slice_tmp_32" + (Compiler.CompilationModel.Expr.bitAnd + (Compiler.CompilationModel.Expr.localVar "_verity_slice_tmp_31") + (Compiler.CompilationModel.Expr.literal 340282366920938463463374607431768211455)), + Compiler.CompilationModel.Stmt.assignVar + "_verity_slice_tmp_33" + (Compiler.CompilationModel.Expr.localVar "_verity_slice_tmp_32")] + [Compiler.CompilationModel.Stmt.assignVar + "_verity_slice_tmp_33" + (Compiler.CompilationModel.Expr.literal 0)], + Compiler.CompilationModel.Stmt.letVar + "fee" + (Compiler.CompilationModel.Expr.localVar "_verity_slice_tmp_33"), + Compiler.CompilationModel.Stmt.letVar + "_verity_slice_tmp_34" + (Compiler.CompilationModel.Expr.bitAnd + (Compiler.CompilationModel.Expr.localVar "post") + (Compiler.CompilationModel.Expr.literal 340282366920938463463374607431768211455)), + Compiler.CompilationModel.Stmt.letVar + "_verity_slice_tmp_35" + (Compiler.CompilationModel.Expr.localVar "_verity_slice_tmp_34"), + Compiler.CompilationModel.Stmt.letVar + "_verity_slice_tmp_36" + (Compiler.CompilationModel.Expr.literal 0), + Compiler.CompilationModel.Stmt.ite + (Compiler.CompilationModel.Expr.lt + (Compiler.CompilationModel.Expr.localVar "_verity_slice_tmp_35") + (Compiler.CompilationModel.Expr.localVar "fee")) + [Compiler.CompilationModel.Stmt.panic (Verity.Core.PanicCode.arithmeticOverflow)] + [Compiler.CompilationModel.Stmt.assignVar + "_verity_slice_tmp_36" + (Compiler.CompilationModel.Expr.sub + (Compiler.CompilationModel.Expr.localVar "_verity_slice_tmp_35") + (Compiler.CompilationModel.Expr.localVar "fee"))], + Compiler.CompilationModel.Stmt.letVar + "_verity_slice_tmp_37" + (Compiler.CompilationModel.Expr.localVar "_verity_slice_tmp_36"), + Compiler.CompilationModel.Stmt.letVar + "_verity_slice_tmp_38" + (Compiler.CompilationModel.Expr.bitAnd + (Compiler.CompilationModel.Expr.localVar "postFee") + (Compiler.CompilationModel.Expr.literal 340282366920938463463374607431768211455)), + Compiler.CompilationModel.Stmt.letVar + "_verity_slice_tmp_39" + (Compiler.CompilationModel.Expr.localVar "_verity_slice_tmp_38"), + Compiler.CompilationModel.Stmt.letVar + "_verity_slice_tmp_40" + (Compiler.CompilationModel.Expr.literal 0), + Compiler.CompilationModel.Stmt.ite + (Compiler.CompilationModel.Expr.lt + (Compiler.CompilationModel.Expr.localVar "_verity_slice_tmp_39") + (Compiler.CompilationModel.Expr.localVar "fee")) + [Compiler.CompilationModel.Stmt.panic (Verity.Core.PanicCode.arithmeticOverflow)] + [Compiler.CompilationModel.Stmt.assignVar + "_verity_slice_tmp_40" + (Compiler.CompilationModel.Expr.sub + (Compiler.CompilationModel.Expr.localVar "_verity_slice_tmp_39") + (Compiler.CompilationModel.Expr.localVar "fee"))], + Compiler.CompilationModel.Stmt.letVar + "_verity_slice_tmp_41" + (Compiler.CompilationModel.Expr.localVar "_verity_slice_tmp_40"), + Compiler.CompilationModel.Stmt.returnValues + [Compiler.CompilationModel.Expr.localVar "_verity_slice_tmp_37", + Compiler.CompilationModel.Expr.localVar "_verity_slice_tmp_41", + Compiler.CompilationModel.Expr.localVar "fee"]], + modifies := [], + noExternalCalls := false, + allowPostInteractionWrites := false, + nonReentrantLock := none, + ceiSafe := false, + reentrancyTrusted := false, + requiresRole := none, + isInternal := false, + localObligations := [] }], + events := [], + errors := [], + externals := [], + adtTypes := [], + storageNamespace := none } diff --git a/Contracts/SpecAliases.lean b/Contracts/SpecAliases.lean deleted file mode 100644 index f463791293..0000000000 --- a/Contracts/SpecAliases.lean +++ /dev/null @@ -1,8 +0,0 @@ -import Contracts.Specs - -/-! -Deprecated compatibility import. - -Import the canonical contract module and use `Contracts..spec` in new -code. This forwarding module intentionally contains no legacy aliases. --/ diff --git a/Contracts/TypedIRTests.lean b/Contracts/TypedIRTests.lean deleted file mode 100644 index 3e976c97d1..0000000000 --- a/Contracts/TypedIRTests.lean +++ /dev/null @@ -1,3148 +0,0 @@ -import Verity.Core.Free.TypedIR -import Verity.Core.Free.ContractStep -import Compiler.CompilationModel.Compile -import Compiler.CompilationModel.ExpressionCompile -import Compiler.TypedIRCompiler -import Compiler.TypedIRCompilerCorrectness -import Compiler.TypedIRLowering -import Compiler.Proofs.IRGeneration.IRInterpreter -import Contracts.Specs -import Contracts.Smoke - -namespace Verity.Core.Free - -private def contains (haystack needle : String) : Bool := - let h := haystack.toList - let n := needle.toList - if n.isEmpty then true - else - let rec go : List Char → Bool - | [] => false - | c :: cs => - if (c :: cs).take n.length == n then true - else go cs - go h - -private def tVarIdNamed? (vars : List TVar) (name : String) : Option Nat := - match vars with - | [] => none - | v :: rest => if tVarName v == name then some v.id else tVarIdNamed? rest name - -private def returnedWordName? : List Compiler.Yul.YulStmt → Option String - | [] => none - | .exprStmt (.call "mstore" [.lit 0, .ident name]) :: .exprStmt (.call "return" [.lit 0, .lit 32]) :: _ => - some name - | _ :: rest => returnedWordName? rest - -/-- Recognize `sload(mappingSlot(slot, ident keyName))`. -/ -private def isMappingLoadExpr (slotNum : Nat) (keyName : String) : Compiler.Yul.YulExpr → Bool - | .call "sload" [.call "mappingSlot" [.lit slot', .ident keyName']] => - slot' == slotNum && keyName' == keyName - | _ => false - -def x : TVar := { id := 0, ty := .uint256 } -def y : TVar := { id := 1, ty := .uint256 } -def flag : TVar := { id := 2, ty := .bool } -def owner : TVar := { id := 3, ty := .address } - -/-- Expression typing sanity check: uint arithmetic stays in `Ty.uint256`. -/ -example : TExpr .uint256 := TExpr.add (TExpr.var x) (TExpr.var y) - -/-- Statement typing sanity check: destination and rhs types must match. -/ -example : TStmt := TStmt.assign x (TExpr.add (TExpr.var x) (TExpr.uintLit 1)) - -/-- Branching sanity check: condition must be statically boolean. -/ -example : TStmt := TStmt.if_ - (TExpr.var flag) - [TStmt.setStorage 0 (TExpr.var x)] - [TStmt.revert "bad condition"] - -/-- Block construction sanity check with typed declarations and statements. -/ -example : TBlock where - params := [x] - locals := [y, flag] - body := - [ TStmt.let_ y (TExpr.add (TExpr.var x) (TExpr.uintLit 1)) - , TStmt.assign x (TExpr.var y) - , TStmt.if_ (TExpr.var flag) - [TStmt.setStorage 0 (TExpr.var x)] - [TStmt.revert "flag=false"] - ] - -def baseWorld : Verity.ContractState := - { Verity.defaultState with - sender := 7 - thisAddress := 9 - msgValue := 11 - blockTimestamp := 13 - blockNumber := 17 - chainId := 19 - } - -def baseState : TExecState := - { world := baseWorld - vars := - { uint256 := fun - | 0 => 5 - | 1 => 8 - | _ => 0 - bool := fun - | 2 => true - | _ => false - address := fun - | 3 => 42 - | _ => 0 - } } - -/-- `evalTExpr` reads variables and preserves typed arithmetic. -/ -example : - evalTExpr baseState (TExpr.add (TExpr.var x) (TExpr.var y)) = - Verity.Core.Uint256.add 5 8 := by - simp [baseState, x, y, evalTExpr, TVars.get] - -/-- `evalTExpr` division matches Uint256 semantics (divide by zero returns zero). -/ -example : - evalTExpr baseState (TExpr.div (TExpr.uintLit 42) (TExpr.uintLit 0)) = - (0 : Verity.Core.Uint256) := by - simp [evalTExpr, Verity.Core.Uint256.div] - -/-- `evalTExpr` modulo matches Uint256 semantics (mod by zero returns zero). -/ -example : - evalTExpr baseState (TExpr.mod (TExpr.uintLit 10) (TExpr.uintLit 0)) = - (0 : Verity.Core.Uint256) := by - simp [evalTExpr, Verity.Core.Uint256.mod] - -/-- One-step evaluator reductions are available through the dedicated `ir_step` simp set. -/ -example : - evalTBlock baseState { params := [], locals := [], body := [] } = .ok baseState := by - contract_step - -/-- `contract_step` also simplifies single-step expression reductions. -/ -example : - evalTExpr baseState (TExpr.var x) = (5 : Verity.Core.Uint256) := by - contract_step - simp [baseState, x, TVars.get] - -def compileDivExprSucceeds : Bool := - match (compileStmts [] - [Compiler.CompilationModel.Stmt.return - (Compiler.CompilationModel.Expr.div - (Compiler.CompilationModel.Expr.literal 10) - (Compiler.CompilationModel.Expr.literal 3))]).run {} with - | .ok _ => true - | .error _ => false - -def compileModExprSucceeds : Bool := - match (compileStmts [] - [Compiler.CompilationModel.Stmt.return - (Compiler.CompilationModel.Expr.mod - (Compiler.CompilationModel.Expr.literal 10) - (Compiler.CompilationModel.Expr.literal 3))]).run {} with - | .ok _ => true - | .error _ => false - -def compileChainidLoweringShapeOk : Bool := - match (compileStmts [] - [Compiler.CompilationModel.Stmt.return - Compiler.CompilationModel.Expr.chainid]).run {} with - | .ok st => - let rendered := reprStr st.2.body.toList - contains rendered "returnUint" && - contains rendered "chainid" && - match lowerTStmts st.2.body.toList with - | [ .exprStmt (.call "mstore" [.lit 0, .call "chainid" []]) - , .exprStmt (.call "return" [.lit 0, .lit 32]) ] => true - | _ => false - | .error _ => false - -def compileRawLogSucceeds : Bool := - match (compileStmts [] - [Compiler.CompilationModel.Stmt.rawLog - [Compiler.CompilationModel.Expr.literal 1, Compiler.CompilationModel.Expr.literal 2] - (Compiler.CompilationModel.Expr.literal 0) - (Compiler.CompilationModel.Expr.literal 64)]).run {} with - | .ok _ => true - | .error _ => false - -def compileStorageAddrReturnUsesTypedAddressRead : Bool := - let fields : List Compiler.CompilationModel.Field := - [{ name := "owner", ty := Compiler.CompilationModel.FieldType.address }] - match (compileStmts fields - [Compiler.CompilationModel.Stmt.return - (Compiler.CompilationModel.Expr.storageAddr "owner")]).run {} with - | .ok st => - let rendered := reprStr st.2.body.toList - contains rendered "returnAddr" && - contains rendered "getStorageAddr 0" - | .error _ => false - -def compileSetStorageAddrUsesTypedAddressWrite : Bool := - let fields : List Compiler.CompilationModel.Field := - [{ name := "owner", ty := Compiler.CompilationModel.FieldType.address }] - match (compileStmts fields - [Compiler.CompilationModel.Stmt.setStorageAddr "owner" Compiler.CompilationModel.Expr.caller]).run {} with - | .ok st => - let rendered := reprStr st.2.body.toList - contains rendered "setStorageAddr 0" && - contains rendered "sender" - | .error _ => false - -def compileStorageAddrRejectsNonAddressField : Bool := - let fields : List Compiler.CompilationModel.Field := - [{ name := "count", ty := Compiler.CompilationModel.FieldType.uint256 }] - match (compileStmts fields - [Compiler.CompilationModel.Stmt.return - (Compiler.CompilationModel.Expr.storageAddr "count")]).run {} with - | .ok _ => false - | .error msg => - msg = "Typed IR compile error: storage field 'count' is not address-typed; use Expr.storage instead" - -def compileSetStorageAddrRejectsNonAddressField : Bool := - let fields : List Compiler.CompilationModel.Field := - [{ name := "count", ty := Compiler.CompilationModel.FieldType.uint256 }] - match (compileStmts fields - [Compiler.CompilationModel.Stmt.setStorageAddr "count" Compiler.CompilationModel.Expr.caller]).run {} with - | .ok _ => false - | .error msg => - msg = "Typed IR compile error: setStorageAddr requires an address-typed field 'count'" - -def compileStorageAddrRejectsPackedAddressField : Bool := - let fields : List Compiler.CompilationModel.Field := - [{ name := "owner" - , ty := Compiler.CompilationModel.FieldType.address - , packedBits := some { offset := 32, width := 160 } }] - match (compileStmts fields - [Compiler.CompilationModel.Stmt.return - (Compiler.CompilationModel.Expr.storageAddr "owner")]).run {} with - | .ok _ => false - | .error msg => - msg = "Typed IR compile error: storage field 'owner' uses packedBits; address-typed packed storage is not yet supported in typed IR" - -def compileSetStorageAddrRejectsPackedAddressField : Bool := - let fields : List Compiler.CompilationModel.Field := - [{ name := "owner" - , ty := Compiler.CompilationModel.FieldType.address - , packedBits := some { offset := 32, width := 160 } }] - match (compileStmts fields - [Compiler.CompilationModel.Stmt.setStorageAddr "owner" Compiler.CompilationModel.Expr.caller]).run {} with - | .ok _ => false - | .error msg => - msg = "Typed IR compile error: storage field 'owner' uses packedBits; address-typed packed storage is not yet supported in typed IR" - -def compileStorageRejectsDynamicArrayField : Bool := - let fields : List Compiler.CompilationModel.Field := - [{ name := "queue", ty := Compiler.CompilationModel.FieldType.dynamicArray .uint256 }] - match (compileStmts fields - [Compiler.CompilationModel.Stmt.return - (Compiler.CompilationModel.Expr.storage "queue")]).run {} with - | .ok _ => false - | .error msg => - msg = "Typed IR compile error: storage field 'queue' is a storage dynamic array; use Expr.storageArrayLength or Expr.storageArrayElement instead" - -def compileStorageAddrRejectsDynamicArrayField : Bool := - let fields : List Compiler.CompilationModel.Field := - [{ name := "queue", ty := Compiler.CompilationModel.FieldType.dynamicArray .uint256 }] - match (compileStmts fields - [Compiler.CompilationModel.Stmt.return - (Compiler.CompilationModel.Expr.storageAddr "queue")]).run {} with - | .ok _ => false - | .error msg => - msg = "Typed IR compile error: storage field 'queue' is a storage dynamic array; use Expr.storageArrayLength or Expr.storageArrayElement instead" - -def compileSetStorageRejectsDynamicArrayField : Bool := - let fields : List Compiler.CompilationModel.Field := - [{ name := "queue", ty := Compiler.CompilationModel.FieldType.dynamicArray .uint256 }] - match (compileStmts fields - [Compiler.CompilationModel.Stmt.setStorage "queue" (Compiler.CompilationModel.Expr.literal 1)]).run {} with - | .ok _ => false - | .error msg => - msg = "Typed IR compile error: storage field 'queue' is a storage dynamic array; use Stmt.storageArrayPush, Stmt.storageArrayPop, or Stmt.setStorageArrayElement instead" - -def compileSetStorageAddrRejectsDynamicArrayField : Bool := - let fields : List Compiler.CompilationModel.Field := - [{ name := "queue", ty := Compiler.CompilationModel.FieldType.dynamicArray .uint256 }] - match (compileStmts fields - [Compiler.CompilationModel.Stmt.setStorageAddr "queue" Compiler.CompilationModel.Expr.caller]).run {} with - | .ok _ => false - | .error msg => - msg = "Typed IR compile error: storage field 'queue' is a storage dynamic array; use Stmt.storageArrayPush, Stmt.storageArrayPop, or Stmt.setStorageArrayElement instead" - -def compileYulStorageAddrMasksPackedAddressField : Bool := - let fields : List Compiler.CompilationModel.Field := - [{ name := "owner" - , ty := Compiler.CompilationModel.FieldType.address - , packedBits := some { offset := 32, width := 160 } }] - match Compiler.CompilationModel.compileExpr fields Compiler.CompilationModel.DynamicDataSource.calldata - (Compiler.CompilationModel.Expr.storageAddr "owner") with - | .ok expr => - let rendered := reprStr expr - contains rendered "and" && - contains rendered "shr" && - contains rendered "sload" - | .error _ => false - -def compileYulSetStorageAddrRejectsNonAddressField : Bool := - let fields : List Compiler.CompilationModel.Field := - [{ name := "count", ty := Compiler.CompilationModel.FieldType.uint256 }] - match Compiler.CompilationModel.compileStmt fields [] [] Compiler.CompilationModel.DynamicDataSource.calldata [] false [] - (Compiler.CompilationModel.Stmt.setStorageAddr "count" Compiler.CompilationModel.Expr.caller) with - | .ok _ => false - | .error msg => - msg = "Compilation error: field 'count' is not address-typed; use Stmt.setStorage instead" - -def compileRawLogLoweringShapeOk : Bool := - match (compileStmts [] - [Compiler.CompilationModel.Stmt.rawLog - [Compiler.CompilationModel.Expr.literal 1, Compiler.CompilationModel.Expr.literal 2] - (Compiler.CompilationModel.Expr.literal 0) - (Compiler.CompilationModel.Expr.literal 64)]).run {} with - | .ok st => - match lowerTStmts st.2.body.toList with - | [.exprStmt (.call "log2" [.lit 0, .lit 64, .lit 1, .lit 2])] => true - | _ => false - | .error _ => false - -def compileRawLogTooManyTopicsFails : Bool := - match (compileStmts [] - [Compiler.CompilationModel.Stmt.rawLog - [ Compiler.CompilationModel.Expr.literal 1 - , Compiler.CompilationModel.Expr.literal 2 - , Compiler.CompilationModel.Expr.literal 3 - , Compiler.CompilationModel.Expr.literal 4 - , Compiler.CompilationModel.Expr.literal 5 - ] - (Compiler.CompilationModel.Expr.literal 0) - (Compiler.CompilationModel.Expr.literal 64)]).run {} with - | .ok _ => false - | .error msg => msg = "Typed IR compile error: rawLog supports at most 4 topics, got 5" - -def compileEmitSucceeds : Bool := - match (compileStmts [] - [Compiler.CompilationModel.Stmt.emit "Transfer" - [Compiler.CompilationModel.Expr.literal 1, Compiler.CompilationModel.Expr.literal 2]]).run {} with - | .ok _ => true - | .error _ => false - -def compileEmitTypedShapeOk : Bool := - match (compileStmts [] - [Compiler.CompilationModel.Stmt.emit "Transfer" - [Compiler.CompilationModel.Expr.literal 1, Compiler.CompilationModel.Expr.literal 2]]).run {} with - | .ok st => - match st.2.body.toList with - | [TStmt.emit "Transfer" topics] => - match topics with - | [t1, t2] => - match lowerTExpr t1, lowerTExpr t2 with - | .lit 1, .lit 2 => true - | _, _ => false - | _ => false - | _ => false - | .error _ => false - -def compileEmitLoweringShapeOk : Bool := - match (compileStmts [] - [Compiler.CompilationModel.Stmt.emit "Transfer" - [Compiler.CompilationModel.Expr.literal 1, Compiler.CompilationModel.Expr.literal 2]]).run {} with - | .ok st => - match lowerTStmts st.2.body.toList with - | [.exprStmt (.call "log3" [.lit 0, .lit 0, .lit _, .lit 1, .lit 2])] => true - | _ => false - | .error _ => false - -def compileEmitEncodesDistinctTopic0 : Bool := - match (compileStmts [] - [Compiler.CompilationModel.Stmt.emit "Transfer" - [ Compiler.CompilationModel.Expr.literal 1 - , Compiler.CompilationModel.Expr.literal 2 - ]]).run {}, - (compileStmts [] - [Compiler.CompilationModel.Stmt.emit "Approval" - [ Compiler.CompilationModel.Expr.literal 1 - , Compiler.CompilationModel.Expr.literal 2 - ]]).run {} with - | .ok st1, .ok st2 => - match lowerTStmts st1.2.body.toList, lowerTStmts st2.2.body.toList with - | [.exprStmt (.call "log3" [.lit 0, .lit 0, .lit t1, .lit 1, .lit 2])], - [.exprStmt (.call "log3" [.lit 0, .lit 0, .lit t2, .lit 1, .lit 2])] => t1 != t2 - | _, _ => false - | _, _ => false - -def compileEmitTooManyArgsFails : Bool := - match (compileStmts [] - [Compiler.CompilationModel.Stmt.emit "BigEvent" - [ Compiler.CompilationModel.Expr.literal 1 - , Compiler.CompilationModel.Expr.literal 2 - , Compiler.CompilationModel.Expr.literal 3 - , Compiler.CompilationModel.Expr.literal 4 - ]]).run {} with - | .ok _ => false - | .error msg => msg = "Typed IR compile error: emit supports at most 3 arguments in typed mode, got 4" - -/-- Typed-IR compiler accepts source-level `Expr.div`. -/ -example : compileDivExprSucceeds = true := by native_decide - -/-- Typed-IR compiler accepts source-level `Expr.mod`. -/ -example : compileModExprSucceeds = true := by native_decide - -/-- Typed-IR compiler preserves `chainid` through typed lowering. -/ -example : compileChainidLoweringShapeOk = true := by native_decide - -/-- Typed-IR compiler accepts source-level `Stmt.rawLog`. -/ -example : compileRawLogSucceeds = true := by native_decide - -/-- Address-typed storage reads stay explicit through typed IR compilation. -/ -example : compileStorageAddrReturnUsesTypedAddressRead = true := by native_decide - -/-- Address-typed storage writes stay explicit through typed IR compilation. -/ -example : compileSetStorageAddrUsesTypedAddressWrite = true := by native_decide - -/-- Typed-IR rejects `Expr.storageAddr` on non-address fields. -/ -example : compileStorageAddrRejectsNonAddressField = true := by native_decide - -/-- Typed-IR rejects `Stmt.setStorageAddr` on non-address fields. -/ -example : compileSetStorageAddrRejectsNonAddressField = true := by native_decide - -/-- Typed-IR rejects packed address storage reads until the abstract storage model supports them. -/ -example : compileStorageAddrRejectsPackedAddressField = true := by native_decide - -/-- Typed-IR rejects packed address storage writes until the abstract storage model supports them. -/ -example : compileSetStorageAddrRejectsPackedAddressField = true := by native_decide - -/-- Typed-IR rejects scalar-style reads from storage dynamic arrays. -/ -example : compileStorageRejectsDynamicArrayField = true := by native_decide - -/-- Typed-IR rejects address-style reads from storage dynamic arrays. -/ -example : compileStorageAddrRejectsDynamicArrayField = true := by native_decide - -/-- Typed-IR rejects scalar-style writes to storage dynamic arrays. -/ -example : compileSetStorageRejectsDynamicArrayField = true := by native_decide - -/-- Typed-IR rejects address-style writes to storage dynamic arrays. -/ -example : compileSetStorageAddrRejectsDynamicArrayField = true := by native_decide - -/-- Yul address-field reads preserve packed masking when the field is sub-word. -/ -example : compileYulStorageAddrMasksPackedAddressField = true := by native_decide - -/-- Yul compilation rejects `Stmt.setStorageAddr` on non-address fields. -/ -example : compileYulSetStorageAddrRejectsNonAddressField = true := by native_decide - -/-- Typed-IR compiler emits the expected typed `rawLog` statement shape. -/ -example : compileRawLogLoweringShapeOk = true := by native_decide - -/-- Typed-IR compiler rejects `rawLog` with more than 4 topics. -/ -example : compileRawLogTooManyTopicsFails = true := by native_decide - -/-- Typed-IR compiler accepts source-level `Stmt.emit`. -/ -example : compileEmitSucceeds = true := by native_decide - -/-- Typed-IR compiler lowers `Stmt.emit` into first-class typed `TStmt.emit`. -/ -example : compileEmitTypedShapeOk = true := by native_decide - -/-- Typed-IR compiler lowers `TStmt.emit` to `logN` with empty data payload. -/ -example : compileEmitLoweringShapeOk = true := by native_decide - -/-- Typed-IR compiler preserves event identity via a distinct topic0. -/ -example : compileEmitEncodesDistinctTopic0 = true := by native_decide - -/-- Typed-IR compiler rejects `Stmt.emit` with more than 3 typed-mode args. -/ -example : compileEmitTooManyArgsFails = true := by native_decide - -/-- Context expressions read from world/environment. -/ -example : - evalTExpr baseState TExpr.sender = (7 : Verity.Core.Address) := by - simp [baseState, evalTExpr, baseWorld, Verity.Env.ofWorld] - -example : - evalTExpr baseState TExpr.blockNumber = (17 : Verity.Core.Uint256) := by - simp [baseState, evalTExpr, baseWorld, Verity.Env.ofWorld] - -example : - evalTExpr baseState TExpr.chainid = (19 : Verity.Core.Uint256) := by - simp [baseState, evalTExpr, baseWorld, Verity.Env.ofWorld] - -def envOverrideState : TExecState := - { world := baseWorld - env := { sender := 99, thisAddress := 100, msgValue := 101, blockTimestamp := 102, blockNumber := 103, chainId := 104 } - vars := baseState.vars } - -/-- Context expressions read from explicit `TExecState.env`, not from world fields. -/ -example : - evalTExpr envOverrideState TExpr.sender = (99 : Verity.Core.Address) := by - simp [envOverrideState, evalTExpr] - -example : - evalTExpr envOverrideState TExpr.chainid = (104 : Verity.Core.Uint256) := by - simp [envOverrideState, evalTExpr] - -/-- Storage updates do not mutate explicit execution environment fields. -/ -example : - match evalTStmt envOverrideState (TStmt.setStorage 8 (TExpr.uintLit 55)) with - | .ok s' => s'.env.sender = (99 : Verity.Core.Address) - | .revert _ => False := by - simp [envOverrideState, evalTStmt, evalTStmtFuel, defaultEvalFuel] - -/-- Assignment updates the typed variable environment. -/ -example : - match evalTStmt baseState (TStmt.assign x (TExpr.uintLit 99)) with - | .ok s' => s'.vars.get x = (99 : Verity.Core.Uint256) - | .revert _ => True := by - simp [evalTStmt, evalTStmtFuel, defaultEvalFuel] - -/-- Storage updates are reflected in the output execution world. -/ -example : - match evalTStmt baseState (TStmt.setStorage 4 (TExpr.uintLit 123)) with - | .ok s' => s'.world.storage 4 = (123 : Verity.Core.Uint256) - | .revert _ => True := by - simp [evalTStmt, evalTStmtFuel, defaultEvalFuel] - -/-- Raw logs are recorded as append-only event entries. -/ -example : - match evalTStmt baseState (TStmt.rawLog [TExpr.uintLit 1, TExpr.uintLit 2] (TExpr.uintLit 0) (TExpr.uintLit 64)) with - | .ok s' => s'.world.events.length = baseState.world.events.length + 1 - | .revert _ => False := by - simp [evalTStmt, evalTStmtFuel, defaultEvalFuel] - rfl - -/-- Typed emits are recorded as append-only event entries. -/ -example : - match evalTStmt baseState (TStmt.emit "Transfer" [TExpr.uintLit 1, TExpr.uintLit 2]) with - | .ok s' => s'.world.events.length = baseState.world.events.length + 1 - | .revert _ => False := by - simp [evalTStmt, evalTStmtFuel, defaultEvalFuel] - rfl - -/-- Branching and block execution compose through `evalTBlock`. -/ -example : - match evalTStmt baseState - (TStmt.if_ (TExpr.boolLit true) - [] - [TStmt.revert "no"]) with - | .ok _ => True - | .revert _ => False := by - simp [evalTStmt, evalTStmtFuel, evalTStmtsFuel, defaultEvalFuel] - -/-- Explicit revert in the block propagates as `Except.error`. -/ -example : - evalTBlock baseState - { params := [] - locals := [] - body := [TStmt.revert "halt"] } = .revert "halt" := by - simp [evalTBlock, evalTStmts, evalTStmtsFuel, evalTStmtFuel, defaultEvalFuel] - -open Compiler.Yul -open Compiler.Proofs.IRGeneration - -/-- Lowering emits a Yul `div` call for typed division. -/ -example : - lowerTExpr (TExpr.div (TExpr.uintLit 10) (TExpr.uintLit 3)) = - .call "div" [.lit 10, .lit 3] := by - rfl - -/-- Lowering emits a Yul `mod` call for typed modulo. -/ -example : - lowerTExpr (TExpr.mod (TExpr.uintLit 10) (TExpr.uintLit 3)) = - .call "mod" [.lit 10, .lit 3] := by - rfl - -/-- Lowering emits `log2` for typed raw logs with two topics. -/ -example : - lowerTStmts [TStmt.rawLog [TExpr.uintLit 1, TExpr.uintLit 2] (TExpr.uintLit 0) (TExpr.uintLit 64)] = - [.exprStmt (.call "log2" [.lit 0, .lit 64, .lit 1, .lit 2])] := by - rfl - -/-- Lowering emits `log0` when the topic list is empty. -/ -example : - lowerTStmts [TStmt.rawLog [] (TExpr.uintLit 0) (TExpr.uintLit 32)] = - [.exprStmt (.call "log0" [.lit 0, .lit 32])] := by - rfl - -/-- Lowering emits `log3` for typed `emit` with two indexed args. -/ -example : - lowerTStmts [TStmt.emit "Transfer" [TExpr.uintLit 1, TExpr.uintLit 2]] = - [.exprStmt (.call "log3" [.lit 0, .lit 0, .lit (typedEventNameTopicWord "Transfer"), .lit 1, .lit 2])] := by - rfl - -def counterTmp : TVar := { id := 10, ty := .uint256 } - -/-- Typed IR block equivalent to Counter.increment (`count := count + 1`). -/ -def counterIncrementTBlock : TBlock where - params := [] - locals := [counterTmp] - body := - [ TStmt.let_ counterTmp (TExpr.getStorage 0) - , TStmt.assign counterTmp (TExpr.add (TExpr.var counterTmp) (TExpr.uintLit 1)) - , TStmt.setStorage 0 (TExpr.var counterTmp) - ] - -/-- Existing untyped IR program equivalent to `counterIncrementTBlock`. -/ -def counterIncrementIR : List YulStmt := - [ .let_ "tmp" (.call "sload" [.lit 0]) - , .assign "tmp" (.call "add" [.ident "tmp", .lit 1]) - , .exprStmt (.call "sstore" [.lit 0, .ident "tmp"]) - ] - -def counterTypedInitWorld : Verity.ContractState := - Verity.defaultState.writeSlot 0 41 - -def counterTypedInit : TExecState := - { world := counterTypedInitWorld } - -def counterIRInit : IRState := - { vars := [] - «storage» := fun i => if i = 0 then 41 else 0 - memory := fun _ => 0 - calldata := [] - returnValue := none - sender := 0 - msgValue := 0 - thisAddress := 0 - blockTimestamp := 0 - blockNumber := 0 - chainId := 0 - blobBaseFee := 0 - selector := 0 - events := [] } - -def counterTypedFinalSlot : Option Nat := - match evalTBlock counterTypedInit counterIncrementTBlock with - | .ok s => some ((s.world.storage 0 : Verity.Core.Uint256) : Nat) - | .revert _ => none - -def counterIRFinalSlot : Option Nat := - match execIRStmts 32 counterIRInit counterIncrementIR with - | .continue s => some (s.storage 0) - | _ => none - -/-- Golden test: typed Counter increment matches existing IR evaluation. -/ -example : counterTypedFinalSlot = counterIRFinalSlot := by - native_decide - -def simpleStorageValue : TVar := { id := 20, ty := .uint256 } - -/-- Typed IR block equivalent to SimpleStorage.store(v). -/ -def simpleStorageStoreTBlock : TBlock where - params := [simpleStorageValue] - locals := [] - body := [TStmt.setStorage 0 (TExpr.var simpleStorageValue)] - -/-- Existing untyped IR program equivalent to `simpleStorageStoreTBlock`. -/ -def simpleStorageStoreIR : List YulStmt := - [ .exprStmt (.call "sstore" [.lit 0, .ident "value"]) ] - -def simpleStorageTypedInitWorld : Verity.ContractState := - Verity.defaultState.writeSlot 0 5 - -def simpleStorageTypedInit : TExecState := - { world := simpleStorageTypedInitWorld - vars := { uint256 := fun - | 20 => 77 - | _ => 0 } } - -def simpleStorageIRInit : IRState := - (IRState.initial 0).setVar "value" 77 - -def simpleStorageIRInitWithStorage : IRState := - { vars := simpleStorageIRInit.vars - «storage» := fun i => if i = 0 then 5 else 0 - memory := simpleStorageIRInit.memory - calldata := simpleStorageIRInit.calldata - returnValue := simpleStorageIRInit.returnValue - sender := simpleStorageIRInit.sender - msgValue := simpleStorageIRInit.msgValue - thisAddress := simpleStorageIRInit.thisAddress - blockTimestamp := simpleStorageIRInit.blockTimestamp - blockNumber := simpleStorageIRInit.blockNumber - chainId := simpleStorageIRInit.chainId - blobBaseFee := simpleStorageIRInit.blobBaseFee - selector := simpleStorageIRInit.selector - events := simpleStorageIRInit.events } - -def simpleStorageTypedFinalSlot : Option Nat := - match evalTBlock simpleStorageTypedInit simpleStorageStoreTBlock with - | .ok s => some ((s.world.storage 0 : Verity.Core.Uint256) : Nat) - | .revert _ => none - -def simpleStorageIRFinalSlot : Option Nat := - match execIRStmts 16 simpleStorageIRInitWithStorage simpleStorageStoreIR with - | .continue s => some (s.storage 0) - | _ => none - -/-- Golden test: typed SimpleStorage store matches existing IR evaluation. -/ -example : simpleStorageTypedFinalSlot = simpleStorageIRFinalSlot := by - native_decide - -private def tVarValueNat (state : Verity.Core.Free.TExecState.{0}) (v : TVar) : Nat := - match v with - | ⟨id, .uint256⟩ => state.vars.uint256 id - | ⟨id, .address⟩ => state.vars.address id - | ⟨id, .bool⟩ => if state.vars.bool id then 1 else 0 - | ⟨_, .unit⟩ => 0 - -private def mkIRStateFromTyped (state : Verity.Core.Free.TExecState.{0}) (block : TBlock) : IRState := by - let initVars : List (String × Nat) := - (block.params ++ block.locals).map (fun v => (tVarName v, tVarValueNat state v)) - -- Merge typed storage fields into flat EVM storage. In the EVM there is a - -- single `sload`/`sstore` namespace; the typed IR model splits it into - -- `storage` (uint256) and `storageAddr` (address) for type safety. - let flatStorage : Nat → Nat := fun i => - let u : Nat := state.world.storage i - let a : Nat := state.world.storageAddr i - if a != 0 then a else u - exact - { vars := initVars - «storage» := flatStorage - memory := fun _ => 0 - calldata := [] - returnValue := none - sender := state.env.sender - msgValue := state.env.msgValue - thisAddress := state.env.thisAddress - blockTimestamp := state.env.blockTimestamp - blockNumber := state.env.blockNumber - chainId := state.env.chainId - blobBaseFee := 0 - selector := 0 - events := [] } - -private def abiHeadRoundtripSpec : Compiler.CompilationModel.CompilationModel := { - name := "AbiHeadRoundtrip" - fields := [] - «constructor» := none - functions := [{ - name := "roundtripHeads" - params := [ - { name := "cfg", ty := Compiler.CompilationModel.ParamType.tuple [Compiler.CompilationModel.ParamType.address, Compiler.CompilationModel.ParamType.uint256] }, - { name := "payload", ty := Compiler.CompilationModel.ParamType.bytes }, - { name := "fixedRecipients", ty := Compiler.CompilationModel.ParamType.fixedArray Compiler.CompilationModel.ParamType.address 2 }, - { name := "recipients", ty := Compiler.CompilationModel.ParamType.array Compiler.CompilationModel.ParamType.address }, - { name := "note", ty := Compiler.CompilationModel.ParamType.string } - ] - returnType := some Compiler.CompilationModel.FieldType.uint256 - body := [ - Compiler.CompilationModel.Stmt.return - (Compiler.CompilationModel.Expr.add - (Compiler.CompilationModel.Expr.add - (Compiler.CompilationModel.Expr.add - (Compiler.CompilationModel.Expr.add - (Compiler.CompilationModel.Expr.param "cfg") - (Compiler.CompilationModel.Expr.param "payload")) - (Compiler.CompilationModel.Expr.param "fixedRecipients")) - (Compiler.CompilationModel.Expr.param "recipients")) - (Compiler.CompilationModel.Expr.param "note")) - ] - }] -} - -private def abiHeadRoundtripExpectedParamTys : List Ty := - [Ty.uint256, Ty.uint256, Ty.uint256, Ty.uint256, Ty.uint256] - -private def abiHeadRoundtripInit (block : TBlock) : Option Verity.Core.Free.TExecState.{0} := - match block.params with - | [cfgParam, payloadParam, fixedRecipientsParam, recipientsParam, noteParam] => - some ({ - world := Verity.defaultState - vars := ({ - uint256 := fun i => - if i = cfgParam.id then 11 - else if i = payloadParam.id then 22 - else if i = fixedRecipientsParam.id then 33 - else if i = recipientsParam.id then 44 - else if i = noteParam.id then 55 - else 0 - } : TVars) - } : Verity.Core.Free.TExecState.{0}) - | _ => none - -private def execLoweredSlot0 (fuel : Nat) (state : IRState) (block : TBlock) : Option Nat := - match execIRStmts fuel state (lowerTBlock block) with - | .continue s => some (s.storage 0) - | .stop s => some (s.storage 0) - | .return _ s => some (s.storage 0) - | .revert _ => none - -private def execLoweredReturn (fuel : Nat) (state : IRState) (block : TBlock) : Option Nat := - match execIRStmts fuel state (lowerTBlock block) with - | .return ret _ => some ret - | _ => none - -private def execLoweredState (fuel : Nat) (state : IRState) (block : TBlock) : Option IRState := - match execIRStmts fuel state (lowerTBlock block) with - | .continue s => some s - | .stop s => some s - | .return _ s => some s - | .revert _ => none - -/-- Golden test: lowering typed Counter block to Yul preserves storage-slot result. -/ -example : - execLoweredSlot0 64 (mkIRStateFromTyped counterTypedInit counterIncrementTBlock) counterIncrementTBlock = - counterTypedFinalSlot := by - native_decide - -/-- Smoke test: complex ABI-head params compile through the typed-IR pipeline. -/ -def compiledAbiHeadRoundtripBlock : Option TBlock := - match compileFunctionNamed abiHeadRoundtripSpec "roundtripHeads" with - | .ok block => some block - | .error _ => none - -example : compiledAbiHeadRoundtripBlock.isSome = true := by - native_decide - -/-- Complex ABI-head params normalize to word-typed typed-IR params end to end. -/ -def compiledAbiHeadRoundtripParamTypes : Bool := - match compileFunctionNamed abiHeadRoundtripSpec "roundtripHeads" with - | .error _ => false - | .ok block => decide (block.params.map TVar.ty = abiHeadRoundtripExpectedParamTys) - -example : compiledAbiHeadRoundtripParamTypes = true := by - native_decide - -/-- End-to-end typed-IR lowering preserves the selected ABI-head words. -/ -def compiledAbiHeadRoundtripReturn : Option Nat := - match compileFunctionNamed abiHeadRoundtripSpec "roundtripHeads" with - | .error _ => none - | .ok block => - match abiHeadRoundtripInit block with - | none => none - | some init => - execLoweredReturn 256 (mkIRStateFromTyped init block) block - -example : compiledAbiHeadRoundtripReturn = some 165 := by - native_decide - -/-- Golden test: lowering typed SimpleStorage block to Yul preserves storage-slot result. -/ -example : - execLoweredSlot0 64 (mkIRStateFromTyped simpleStorageTypedInit simpleStorageStoreTBlock) simpleStorageStoreTBlock = - simpleStorageTypedFinalSlot := by - native_decide - -def compiledCounterIncrementFinalSlot : Option Nat := - match compileFunctionNamed Contracts.Counter.spec "increment" with - | .error _ => none - | .ok block => - match evalTBlock counterTypedInit block with - | .ok s => some ((s.world.storage 0 : Verity.Core.Uint256) : Nat) - | .revert _ => none - -/-- Golden test: CompilationModel->typed-IR compiler preserves Counter.increment storage effect. -/ -example : compiledCounterIncrementFinalSlot = counterIRFinalSlot := by - native_decide - -def compiledSimpleStorageStoreFinalSlot : Option Nat := - match compileFunctionNamed Contracts.SimpleStorage.spec "store" with - | .error _ => none - | .ok block => - match block.params with - | [] => none - | valueParam :: _ => - let init : TExecState := - { world := simpleStorageTypedInitWorld - vars := { uint256 := fun - | i => if i = valueParam.id then 77 else 0 } } - match evalTBlock init block with - | .ok s => some ((s.world.storage 0 : Verity.Core.Uint256) : Nat) - | .revert _ => none - -/-- Golden test: CompilationModel->typed-IR compiler preserves SimpleStorage.store storage effect. -/ -example : compiledSimpleStorageStoreFinalSlot = simpleStorageIRFinalSlot := by - native_decide - -def compiledCounterLoweredFinalSlot : Option Nat := - match compileFunctionNamed Contracts.Counter.spec "increment" with - | .error _ => none - | .ok block => - execLoweredSlot0 256 (mkIRStateFromTyped counterTypedInit block) block - -/-- Golden test: compiled typed Counter block lowers to Yul with matching final storage. -/ -example : compiledCounterLoweredFinalSlot = compiledCounterIncrementFinalSlot := by - native_decide - -def compiledCounterDecrementFinalSlot : Option Nat := - let initWorld : Verity.ContractState := - counterTypedInitWorld.writeSlot 0 41 - let initTyped : TExecState := { world := initWorld } - match compileFunctionNamed Contracts.Counter.spec "decrement" with - | .error _ => none - | .ok block => - match evalTBlock initTyped block with - | .ok s => some ((s.world.storage 0 : Verity.Core.Uint256) : Nat) - | .revert _ => none - -def compiledCounterDecrementLoweredFinalSlot : Option Nat := - let initWorld : Verity.ContractState := - counterTypedInitWorld.writeSlot 0 41 - let initTyped : TExecState := { world := initWorld } - match compileFunctionNamed Contracts.Counter.spec "decrement" with - | .error _ => none - | .ok block => - execLoweredSlot0 256 (mkIRStateFromTyped initTyped block) block - -/-- End-to-end Counter decrement: typed IR lowering preserves storage effect. -/ -example : compiledCounterDecrementLoweredFinalSlot = compiledCounterDecrementFinalSlot := by - native_decide - -def compiledCounterGetCountReturn : Option Nat := - let initWorld : Verity.ContractState := - counterTypedInitWorld.writeSlot 0 41 - let initTyped : TExecState := { world := initWorld } - match compileFunctionNamed Contracts.Counter.spec "getCount" with - | .error _ => none - | .ok block => - execLoweredReturn 256 (mkIRStateFromTyped initTyped block) block - -/-- End-to-end Counter getCount: typed IR pipeline returns slot-0 value. -/ -example : compiledCounterGetCountReturn = some 41 := by - native_decide - -def chainidLoweredReturn : Option Nat := - let block : TBlock := - { params := [] - locals := [] - body := [.returnUint TExpr.chainid] } - execLoweredReturn 32 (mkIRStateFromTyped envOverrideState block) block - -/-- Lowered typed IR reads `chainId` from the explicit execution environment. -/ -example : chainidLoweredReturn = some 104 := by - native_decide - -def compiledSimpleStorageLoweredFinalSlot : Option Nat := - match compileFunctionNamed Contracts.SimpleStorage.spec "store" with - | .error _ => none - | .ok block => - match block.params with - | [] => none - | valueParam :: _ => - let init : Verity.Core.Free.TExecState.{0} := - { world := simpleStorageTypedInitWorld - vars := { uint256 := fun - | i => if i = valueParam.id then 77 else 0 } } - execLoweredSlot0 256 (mkIRStateFromTyped init block) block - -/-- Golden test: compiled typed SimpleStorage block lowers to Yul with matching final storage. -/ -example : compiledSimpleStorageLoweredFinalSlot = compiledSimpleStorageStoreFinalSlot := by - native_decide - -def compiledSimpleStorageRetrieveReturn : Option Nat := - let initWorld : Verity.ContractState := - simpleStorageTypedInitWorld.writeSlot 0 77 - let initTyped : TExecState := { world := initWorld } - match compileFunctionNamed Contracts.SimpleStorage.spec "retrieve" with - | .error _ => none - | .ok block => - execLoweredReturn 256 (mkIRStateFromTyped initTyped block) block - -/-- End-to-end SimpleStorage retrieve: typed IR pipeline returns slot-0 value. -/ -example : compiledSimpleStorageRetrieveReturn = some 77 := by - native_decide - -def compiledSimpleStorageStoreRetrieveRoundtrip : Option Nat := - match compileFunctionNamed Contracts.SimpleStorage.spec "store", - compileFunctionNamed Contracts.SimpleStorage.spec "retrieve" with - | .ok storeBlock, .ok retrieveBlock => - match storeBlock.params with - | [] => none - | valueParam :: _ => - let init : Verity.Core.Free.TExecState.{0} := - { world := simpleStorageTypedInitWorld - vars := { uint256 := fun - | i => if i = valueParam.id then 99 else 0 } } - match execLoweredState 256 (mkIRStateFromTyped init storeBlock) storeBlock with - | none => none - | some postStore => - execLoweredReturn 256 postStore retrieveBlock - | _, _ => none - -/-- End-to-end SimpleStorage store→retrieve roundtrip through typed IR pipeline. -/ -example : compiledSimpleStorageStoreRetrieveRoundtrip = some 99 := by - native_decide - -/-- Smoke test: Ledger.transfer compiles successfully (exercises bool→uint256 coercion). -/ -def compiledLedgerTransferBlock : Option TBlock := - match compileFunctionNamed Contracts.Ledger.spec "transfer" with - | .ok block => some block - | .error _ => none - -example : compiledLedgerTransferBlock.isSome = true := by - native_decide - -/-- End-to-end Ledger.transfer: non-self transfer updates both balances correctly. -/ -def compiledLedgerTransferResult : Option (Nat × Nat) := - match compileFunctionNamed Contracts.Ledger.spec "transfer" with - | .error _ => none - | .ok block => - match block.params with - | [toParam, amountParam] => - let initWorld : Verity.ContractState := - -- sender (address 1) has balance 100 in mapping slot 0 - { (Verity.defaultState.writeMap 0 1 100).writeMap 0 2 50 with sender := 1 } - let init : TExecState := - { world := initWorld - vars := { address := fun i => - if i = toParam.id then 2 else 0 - uint256 := fun i => - if i = amountParam.id then 30 else 0 } } - match evalTBlock init block with - | .ok s => some (s.world.storageMap 0 1, s.world.storageMap 0 2) - | .revert _ => none - | _ => none - -/-- Ledger.transfer(to=2, amount=30): sender balance 100→70, recipient balance 50→80. -/ -example : compiledLedgerTransferResult = some (70, 80) := by - native_decide - -/-- Smoke test: Owned.getOwner compiles successfully (exercises returnAddr path). -/ -def compiledOwnedGetOwnerBlock : Option TBlock := - match compileFunctionNamed Contracts.Owned.spec "getOwner" with - | .ok block => some block - | .error _ => none - -example : compiledOwnedGetOwnerBlock.isSome = true := by - native_decide - -/-- End-to-end Owned.getOwner: returns the stored owner address via typed IR lowering. -/ -def compiledOwnedGetOwnerReturn : Option Nat := - match compileFunctionNamed Contracts.Owned.spec "getOwner" with - | .error _ => none - | .ok block => - let initWorld : Verity.ContractState := - Verity.defaultState.writeAddrSlot 0 42 - let init : TExecState := { world := initWorld } - execLoweredReturn 256 (mkIRStateFromTyped init block) block - -/-- Owned.getOwner returns stored owner address (42) through the full pipeline. -/ -example : compiledOwnedGetOwnerReturn = some 42 := by - native_decide - -/-- Smoke test: Owned.transferOwnership compiles successfully (exercises requireOwner + setStorageAddr). -/ -def compiledOwnedTransferOwnershipBlock : Option TBlock := - match compileFunctionNamed Contracts.Owned.spec "transferOwnership" with - | .ok block => some block - | .error _ => none - -example : compiledOwnedTransferOwnershipBlock.isSome = true := by - native_decide - -/-- End-to-end Owned.transferOwnership (happy path): owner transfers ownership. -/ -def compiledOwnedTransferOwnershipSuccess : Option Nat := - match compileFunctionNamed Contracts.Owned.spec "transferOwnership" with - | .error _ => none - | .ok block => - match block.params with - | [newOwnerParam] => - let initWorld : Verity.ContractState := - { Verity.defaultState.writeAddrSlot 0 42 with sender := 42 } - let init : TExecState := - { world := initWorld - vars := { address := fun i => - if i = newOwnerParam.id then 99 else 0 } } - match evalTBlock init block with - | .ok s => some (s.world.storageAddr 0 : Nat) - | .revert _ => none - | _ => none - -/-- Owned.transferOwnership(newOwner=99) by owner (42): owner slot becomes 99. -/ -example : compiledOwnedTransferOwnershipSuccess = some 99 := by native_decide - -/-- End-to-end Owned.transferOwnership (revert path): non-owner is rejected. -/ -def compiledOwnedTransferOwnershipReverts : Bool := - match compileFunctionNamed Contracts.Owned.spec "transferOwnership" with - | .error _ => false - | .ok block => - match block.params with - | [newOwnerParam] => - let initWorld : Verity.ContractState := - { Verity.defaultState.writeAddrSlot 0 42 with sender := 99 } - let init : TExecState := - { world := initWorld - vars := { address := fun i => - if i = newOwnerParam.id then 99 else 0 } } - match evalTBlock init block with - | .ok _ => false - | .revert _ => true - | _ => false - -/-- Owned.transferOwnership by non-owner (99): reverts with "Not owner". -/ -example : compiledOwnedTransferOwnershipReverts = true := by native_decide - -/-- Helper: check that compilation succeeds. -/ -private def compilesOk (spec : Compiler.CompilationModel.CompilationModel) (fn : String) : Bool := - match compileFunctionNamed spec fn with - | .ok _ => true - | .error _ => false - -/-- Smoke test: all Ledger spec functions compile to typed IR. -/ -example : compilesOk Contracts.Ledger.spec "deposit" = true := by native_decide -example : compilesOk Contracts.Ledger.spec "withdraw" = true := by native_decide -example : compilesOk Contracts.Ledger.spec "getBalance" = true := by native_decide - -/-- Smoke test: all Vault spec functions compile to typed IR. -/ -example : compilesOk Contracts.Vault.spec "deposit" = true := by native_decide -example : compilesOk Contracts.Vault.spec "withdraw" = true := by native_decide -example : compilesOk Contracts.Vault.spec "balanceOf" = true := by native_decide -example : compilesOk Contracts.Vault.spec "totalAssets" = true := by native_decide -example : compilesOk Contracts.Vault.spec "totalSupply" = true := by native_decide - -/-- End-to-end Vault.deposit: sender shares and 1:1 totals increase together. -/ -def compiledVaultDepositResult : Option (Nat × Nat × Nat) := - match compileFunctionNamed Contracts.Vault.spec "deposit" with - | .error _ => none - | .ok block => - match block.params with - | [amountParam] => - let initWorld : Verity.ContractState := - { ((Verity.defaultState.writeSlot 0 100).writeSlot 1 100).writeMap 2 1 100 with - sender := 1 } - let init : TExecState := - { world := initWorld - vars := { uint256 := fun i => - if i = amountParam.id then 50 else 0 } } - match evalTBlock init block with - | .ok s => some (s.world.storageMap 2 1, s.world.storage 0, s.world.storage 1) - | .revert _ => none - | _ => none - -/-- Vault.deposit(amount=50): shares 100→150, totalAssets 100→150, totalSupply 100→150. -/ -example : compiledVaultDepositResult = some (150, 150, 150) := by native_decide - -/-- End-to-end Vault.withdraw (happy path): sufficient shares burn 1:1 from balances and totals. -/ -def compiledVaultWithdrawSuccess : Option (Nat × Nat × Nat) := - match compileFunctionNamed Contracts.Vault.spec "withdraw" with - | .error _ => none - | .ok block => - match block.params with - | [sharesParam] => - let initWorld : Verity.ContractState := - { ((Verity.defaultState.writeSlot 0 100).writeSlot 1 100).writeMap 2 1 100 with - sender := 1 } - let init : TExecState := - { world := initWorld - vars := { uint256 := fun i => - if i = sharesParam.id then 30 else 0 } } - match evalTBlock init block with - | .ok s => some (s.world.storageMap 2 1, s.world.storage 0, s.world.storage 1) - | .revert _ => none - | _ => none - -/-- Vault.withdraw(shares=30): shares 100→70, totalAssets 100→70, totalSupply 100→70. -/ -example : compiledVaultWithdrawSuccess = some (70, 70, 70) := by native_decide - -/-- End-to-end Vault.withdraw (revert path): insufficient shares are rejected. -/ -def compiledVaultWithdrawReverts : Bool := - match compileFunctionNamed Contracts.Vault.spec "withdraw" with - | .error _ => false - | .ok block => - match block.params with - | [sharesParam] => - let initWorld : Verity.ContractState := - { ((Verity.defaultState.writeSlot 0 100).writeSlot 1 100).writeMap 2 1 10 with - sender := 1 } - let init : TExecState := - { world := initWorld - vars := { uint256 := fun i => - if i = sharesParam.id then 50 else 0 } } - match evalTBlock init block with - | .ok _ => false - | .revert _ => true - | _ => false - -/-- Vault.withdraw(shares=50) with only 10 shares: reverts. -/ -example : compiledVaultWithdrawReverts = true := by native_decide - -/-- End-to-end Ledger.deposit: increases sender balance via mapping write. -/ -def compiledLedgerDepositResult : Option Nat := - match compileFunctionNamed Contracts.Ledger.spec "deposit" with - | .error _ => none - | .ok block => - match block.params with - | [amountParam] => - let initWorld : Verity.ContractState := - { Verity.defaultState.writeMap 0 1 100 with sender := 1 } - let init : TExecState := - { world := initWorld - vars := { uint256 := fun i => - if i = amountParam.id then 50 else 0 } } - match evalTBlock init block with - | .ok s => some (s.world.storageMap 0 1) - | .revert _ => none - | _ => none - -/-- Ledger.deposit(amount=50): sender balance 100→150. -/ -example : compiledLedgerDepositResult = some 150 := by native_decide - -/-- End-to-end Ledger.withdraw (happy path): sufficient balance succeeds. -/ -def compiledLedgerWithdrawSuccess : Option Nat := - match compileFunctionNamed Contracts.Ledger.spec "withdraw" with - | .error _ => none - | .ok block => - match block.params with - | [amountParam] => - let initWorld : Verity.ContractState := - { Verity.defaultState.writeMap 0 1 100 with sender := 1 } - let init : TExecState := - { world := initWorld - vars := { uint256 := fun i => - if i = amountParam.id then 30 else 0 } } - match evalTBlock init block with - | .ok s => some (s.world.storageMap 0 1) - | .revert _ => none - | _ => none - -/-- Ledger.withdraw(amount=30): sender balance 100→70 (require passes). -/ -example : compiledLedgerWithdrawSuccess = some 70 := by native_decide - -/-- End-to-end Ledger.withdraw (revert path): insufficient balance reverts. -/ -def compiledLedgerWithdrawReverts : Bool := - match compileFunctionNamed Contracts.Ledger.spec "withdraw" with - | .error _ => false - | .ok block => - match block.params with - | [amountParam] => - let initWorld : Verity.ContractState := - { Verity.defaultState.writeMap 0 1 10 with sender := 1 } - let init : TExecState := - { world := initWorld - vars := { uint256 := fun i => - if i = amountParam.id then 50 else 0 } } - match evalTBlock init block with - | .ok _ => false - | .revert _ => true - | _ => false - -/-- Ledger.withdraw(amount=50) with balance=10: reverts with "Insufficient balance". -/ -example : compiledLedgerWithdrawReverts = true := by native_decide - -/-- End-to-end OwnedCounter.increment (happy path): owner can increment. -/ -def compiledOwnedCounterIncrementSuccess : Option Nat := - match compileFunctionNamed Contracts.OwnedCounter.spec "increment" with - | .error _ => none - | .ok block => - let initWorld : Verity.ContractState := - { (Verity.defaultState.writeAddrSlot 0 42).writeSlot 1 10 with sender := 42 } - let init : TExecState := { world := initWorld } - match evalTBlock init block with - | .ok s => some (s.world.storage 1) - | .revert _ => none - -/-- OwnedCounter.increment by owner (42): count 10→11. -/ -example : compiledOwnedCounterIncrementSuccess = some 11 := by native_decide - -/-- End-to-end OwnedCounter.increment (revert path): non-owner is rejected. -/ -def compiledOwnedCounterIncrementReverts : Bool := - match compileFunctionNamed Contracts.OwnedCounter.spec "increment" with - | .error _ => false - | .ok block => - let initWorld : Verity.ContractState := - { (Verity.defaultState.writeAddrSlot 0 42).writeSlot 1 10 with sender := 99 } - let init : TExecState := { world := initWorld } - match evalTBlock init block with - | .ok _ => false - | .revert _ => true - -/-- OwnedCounter.increment by non-owner (99): reverts. -/ -example : compiledOwnedCounterIncrementReverts = true := by native_decide - -/-- Smoke test: all OwnedCounter spec functions compile to typed IR. -/ -example : compilesOk Contracts.OwnedCounter.spec "increment" = true := by native_decide -example : compilesOk Contracts.OwnedCounter.spec "decrement" = true := by native_decide -example : compilesOk Contracts.OwnedCounter.spec "getCount" = true := by native_decide -example : compilesOk Contracts.OwnedCounter.spec "getOwner" = true := by native_decide -example : compilesOk Contracts.OwnedCounter.spec "transferOwnership" = true := by native_decide - -/-- End-to-end OwnedCounter.transferOwnership (happy path): owner transfers ownership, count unchanged. -/ -def compiledOwnedCounterTransferOwnershipSuccess : Option (Nat × Nat) := - match compileFunctionNamed Contracts.OwnedCounter.spec "transferOwnership" with - | .error _ => none - | .ok block => - match block.params with - | [newOwnerParam] => - let initWorld : Verity.ContractState := - { (Verity.defaultState.writeAddrSlot 0 42).writeSlot 1 10 with sender := 42 } - let init : TExecState := - { world := initWorld - vars := { address := fun i => - if i = newOwnerParam.id then 99 else 0 } } - match evalTBlock init block with - | .ok s => some ((s.world.storageAddr 0 : Nat), s.world.storage 1) - | .revert _ => none - | _ => none - -/-- OwnedCounter.transferOwnership(newOwner=99) by owner (42): owner→99, count stays 10. -/ -example : compiledOwnedCounterTransferOwnershipSuccess = some (99, 10) := by native_decide - -/-- End-to-end OwnedCounter.transferOwnership (revert path): non-owner is rejected. -/ -def compiledOwnedCounterTransferOwnershipReverts : Bool := - match compileFunctionNamed Contracts.OwnedCounter.spec "transferOwnership" with - | .error _ => false - | .ok block => - match block.params with - | [newOwnerParam] => - let initWorld : Verity.ContractState := - { (Verity.defaultState.writeAddrSlot 0 42).writeSlot 1 10 with sender := 77 } - let init : TExecState := - { world := initWorld - vars := { address := fun i => - if i = newOwnerParam.id then 99 else 0 } } - match evalTBlock init block with - | .ok _ => false - | .revert _ => true - | _ => false - -/-- OwnedCounter.transferOwnership by non-owner (77): reverts. -/ -example : compiledOwnedCounterTransferOwnershipReverts = true := by native_decide - -/-- Smoke test: all SimpleToken spec functions compile to typed IR. -/ -example : compilesOk Contracts.SimpleToken.spec "mint" = true := by native_decide -example : compilesOk Contracts.SimpleToken.spec "transfer" = true := by native_decide -example : compilesOk Contracts.SimpleToken.spec "balanceOf" = true := by native_decide -example : compilesOk Contracts.SimpleToken.spec "totalSupply" = true := by native_decide -example : compilesOk Contracts.SimpleToken.spec "owner" = true := by native_decide - -private def erc20Spec : Compiler.CompilationModel.CompilationModel := - Contracts.ERC20.spec - -private def maxUint256 : Nat := - 115792089237316195423570985008687907853269984665640564039457584007913129639935 - -/-- Smoke test: all ERC20 spec functions compile to typed IR. -/ -example : compilesOk erc20Spec "mint" = true := by native_decide -example : compilesOk erc20Spec "transfer" = true := by native_decide -example : compilesOk erc20Spec "approve" = true := by native_decide -example : compilesOk erc20Spec "transferFrom" = true := by native_decide -example : compilesOk erc20Spec "balanceOf" = true := by native_decide -example : compilesOk erc20Spec "allowanceOf" = true := by native_decide -example : compilesOk erc20Spec "totalSupply" = true := by native_decide -example : compilesOk erc20Spec "owner" = true := by native_decide - -/-- End-to-end ERC20.transferFrom (normal path): updates balances and decrements allowance. -/ -def compiledERC20TransferFromSuccess : Option (Nat × Nat × Nat) := - match compileFunctionNamed erc20Spec "approve", - compileFunctionNamed erc20Spec "transferFrom" with - | .ok approveBlock, .ok transferFromBlock => - match approveBlock.params, transferFromBlock.params with - | [spenderParam, approveAmountParam], [fromParam, toParam, transferAmountParam] => - let initWorld : Verity.ContractState := - { (Verity.defaultState.writeMap 2 11 100).writeMap 2 33 50 with sender := 11 } - let approveInit : TExecState := - { world := initWorld - vars := { address := fun i => - if i = spenderParam.id then 22 else 0 - uint256 := fun i => - if i = approveAmountParam.id then 40 else 0 } } - match evalTBlock approveInit approveBlock with - | .revert _ => none - | .ok postApprove => - let transferInit : TExecState := - { world := { postApprove.world with sender := 22 } - vars := { address := fun i => - if i = fromParam.id then 11 - else if i = toParam.id then 33 - else 0 - uint256 := fun i => - if i = transferAmountParam.id then 30 else 0 } } - match evalTBlock transferInit transferFromBlock with - | .ok s => - some (s.world.storageMap 2 11, s.world.storageMap 2 33, s.world.storageMap2 3 11 22) - | .revert _ => none - | _, _ => none - | _, _ => none - -/-- ERC20.transferFrom amount=30 after approve(40): from 100→70, to 50→80, allowance 40→10. -/ -example : compiledERC20TransferFromSuccess = some (70, 80, 10) := by native_decide - -/-- End-to-end ERC20.transferFrom (infinite allowance path): allowance is preserved. -/ -def compiledERC20TransferFromInfiniteAllowance : Option (Nat × Nat × Nat) := - match compileFunctionNamed erc20Spec "transferFrom" with - | .error _ => none - | .ok block => - match block.params with - | [fromParam, toParam, amountParam] => - let initWorld : Verity.ContractState := - { ((Verity.defaultState.writeMap 2 11 100).writeMap 2 33 50).writeMap2 3 11 22 - maxUint256 with sender := 22 } - let init : TExecState := - { world := initWorld - vars := { address := fun i => - if i = fromParam.id then 11 - else if i = toParam.id then 33 - else 0 - uint256 := fun i => - if i = amountParam.id then 30 else 0 } } - match evalTBlock init block with - | .ok s => some (s.world.storageMap 2 11, s.world.storageMap 2 33, s.world.storageMap2 3 11 22) - | .revert _ => none - | _ => none - -/-- ERC20.transferFrom with infinite allowance keeps allowance max while moving balances. -/ -example : compiledERC20TransferFromInfiniteAllowance = some (70, 80, maxUint256) := by - native_decide - -/-- End-to-end ERC20.transferFrom self-transfer branch: balances unchanged, allowance decremented. -/ -def compiledERC20TransferFromSelfTransfer : Option (Nat × Nat) := - match compileFunctionNamed erc20Spec "transferFrom" with - | .error _ => none - | .ok block => - match block.params with - | [fromParam, toParam, amountParam] => - let initWorld : Verity.ContractState := - { (Verity.defaultState.writeMap 2 11 100).writeMap2 3 11 22 40 with sender := 22 } - let init : TExecState := - { world := initWorld - vars := { address := fun i => - if i = fromParam.id then 11 - else if i = toParam.id then 11 - else 0 - uint256 := fun i => - if i = amountParam.id then 30 else 0 } } - match evalTBlock init block with - | .ok s => some (s.world.storageMap 2 11, s.world.storageMap2 3 11 22) - | .revert _ => none - | _ => none - -/-- ERC20.transferFrom(from=to=11): balance stays 100 and allowance 40→10. -/ -example : compiledERC20TransferFromSelfTransfer = some (100, 10) := by native_decide - -/-- End-to-end ERC20.transferFrom (revert path): insufficient allowance reverts. -/ -def compiledERC20TransferFromInsufficientAllowanceReverts : Bool := - match compileFunctionNamed erc20Spec "transferFrom" with - | .error _ => false - | .ok block => - match block.params with - | [fromParam, toParam, amountParam] => - let initWorld : Verity.ContractState := - { ((Verity.defaultState.writeMap 2 11 100).writeMap 2 33 50).writeMap2 3 11 22 20 - with sender := 22 } - let init : TExecState := - { world := initWorld - vars := { address := fun i => - if i = fromParam.id then 11 - else if i = toParam.id then 33 - else 0 - uint256 := fun i => - if i = amountParam.id then 30 else 0 } } - match evalTBlock init block with - | .ok _ => false - | .revert _ => true - | _ => false - -/-- ERC20.transferFrom amount=30 with allowance=20 reverts. -/ -example : compiledERC20TransferFromInsufficientAllowanceReverts = true := by native_decide - -private def erc721Spec : Compiler.CompilationModel.CompilationModel := - Contracts.ERC721.spec - -private def addressHelpersSmokeSpec : Compiler.CompilationModel.CompilationModel := - Contracts.Smoke.AddressHelpersSmoke.spec - -private def zeroAddressShadowSmokeSpec : Compiler.CompilationModel.CompilationModel := - Contracts.Smoke.ZeroAddressShadowSmoke.spec - -/-- Smoke test: core ERC721 spec functions compile to typed IR. -/ -example : compilesOk erc721Spec "mint" = true := by native_decide -example : compilesOk erc721Spec "transferFrom" = true := by native_decide -example : compilesOk erc721Spec "ownerOf" = true := by native_decide -example : compilesOk erc721Spec "approve" = true := by native_decide -example : compilesOk erc721Spec "getApproved" = true := by native_decide - -/-- Smoke test: address helper macro functions compile to typed IR. -/ -example : compilesOk addressHelpersSmokeSpec "setDelegate" = true := by native_decide -example : compilesOk addressHelpersSmokeSpec "getDelegate" = true := by native_decide -example : compilesOk addressHelpersSmokeSpec "clearDelegate" = true := by native_decide -example : compilesOk addressHelpersSmokeSpec "hasDelegate" = true := by native_decide -example : compilesOk addressHelpersSmokeSpec "isDelegateZero" = true := by native_decide -example : compilesOk addressHelpersSmokeSpec "setOwnerForId" = true := by native_decide -example : compilesOk addressHelpersSmokeSpec "getOwnerForId" = true := by native_decide -example : compilesOk zeroAddressShadowSmokeSpec "shadowWrite" = true := by native_decide - -/-- Address-keyed mapping helpers write encoded addresses and clear back to zero. -/ -def compiledAddressHelpersSmokeDelegateWrites : Option (Nat × Nat) := - match compileFunctionNamed addressHelpersSmokeSpec "setDelegate", - compileFunctionNamed addressHelpersSmokeSpec "clearDelegate" with - | .ok setBlock, .ok clearBlock => - match setBlock.params, clearBlock.params with - | [setOwnerParam, delegateParam], [clearOwnerParam] => - let setInit : TExecState := - { world := Verity.defaultState - vars := { address := fun i => - if i = setOwnerParam.id then 11 - else if i = delegateParam.id then 42 - else 0 } } - match evalTBlock setInit setBlock with - | .revert _ => none - | .ok postSet => - let clearInit : TExecState := - { world := postSet.world - vars := { address := fun i => if i = clearOwnerParam.id then 11 else 0 } } - match evalTBlock clearInit clearBlock with - | .ok postClear => some (postSet.world.storageMap 0 11, postClear.world.storageMap 0 11) - | .revert _ => none - | _, _ => none - | _, _ => none - -/-- Address-keyed mapping helpers round-trip address writes and zero-address clearing. -/ -example : compiledAddressHelpersSmokeDelegateWrites = some (42, 0) := by native_decide - -/-- `getDelegate` should compile to a direct slot-0 mapping read and return that address. -/ -def compiledAddressHelpersSmokeGetDelegateBody : Bool := - match compileFunctionNamed addressHelpersSmokeSpec "getDelegate" with - | .error _ => false - | .ok block => - match block.params, lowerTStmts block.body with - | [ownerParam], - [ .let_ delegateName delegateRead - , .exprStmt (.call "mstore" [.lit 0, .ident delegateName']) - , .exprStmt (.call "return" [.lit 0, .lit 32]) - ] => - isMappingLoadExpr 0 (tVarName ownerParam) delegateRead && - delegateName' == delegateName - | _, _ => false - -/-- `getDelegate` keeps address-valued mapping reads on the typed path. -/ -example : compiledAddressHelpersSmokeGetDelegateBody = true := by native_decide - -/-- `isDelegateZero` should compile the explicit zero-address predicate over the mapping read. -/ -def compiledAddressHelpersSmokeIsDelegateZeroBody : Bool := - match compileFunctionNamed addressHelpersSmokeSpec "isDelegateZero" with - | .error _ => false - | .ok block => - match block.params, lowerTStmts block.body with - | [_], stmts => - let rendered := reprStr stmts - contains rendered "mappingSlot" && - contains rendered "\"eq\"" && - contains rendered "\"iszero\"" - | _, _ => false - -/-- `isZeroAddress` lowers to an explicit zero comparison over the typed mapping read. -/ -example : compiledAddressHelpersSmokeIsDelegateZeroBody = true := by native_decide - -/-- End-to-end Uint256→Address helper flow: nonzero owner writes/read back, zero-address is rejected. -/ -def compiledAddressHelpersSmokeUintOwnerFlow : Option (Nat × Bool) := - match compileFunctionNamed addressHelpersSmokeSpec "setOwnerForId" with - | .ok setBlock => - match setBlock.params with - | [tokenParam, ownerParam] => - let setInit : TExecState := - { world := Verity.defaultState - vars := { uint256 := fun i => if i = tokenParam.id then 7 else 0 - address := fun i => if i = ownerParam.id then 99 else 0 } } - match evalTBlock setInit setBlock with - | .revert _ => none - | .ok postSet => - let zeroInit : TExecState := - { world := Verity.defaultState - vars := { uint256 := fun i => if i = tokenParam.id then 7 else 0 - address := fun i => if i = ownerParam.id then 0 else 0 } } - match evalTBlock zeroInit setBlock with - | .revert "Invalid owner" => - some (postSet.world.storageMapUint 1 7, true) - | _ => none - | _ => none - | _ => none - -/-- Uint256-keyed address helpers compile through the macro path and enforce zero-address rejection. -/ -example : compiledAddressHelpersSmokeUintOwnerFlow = some (99, true) := by native_decide - -/-- `getOwnerForId` should compile to a direct slot-1 Uint256-keyed mapping read. -/ -def compiledAddressHelpersSmokeGetOwnerForIdBody : Bool := - match compileFunctionNamed addressHelpersSmokeSpec "getOwnerForId" with - | .error _ => false - | .ok block => - match block.params, lowerTStmts block.body with - | [tokenParam], - [ .let_ ownerName ownerRead - , .exprStmt (.call "mstore" [.lit 0, .ident ownerName']) - , .exprStmt (.call "return" [.lit 0, .lit 32]) - ] => - isMappingLoadExpr 1 (tVarName tokenParam) ownerRead && - ownerName' == ownerName - | _, _ => false - -/-- `getOwnerForId` keeps Uint256-keyed address reads on the typed helper path. -/ -example : compiledAddressHelpersSmokeGetOwnerForIdBody = true := by native_decide - -/-- A parameter named `zeroAddress` must stay a parameter, not collapse to the builtin zero literal. -/ -def compiledZeroAddressShadowWrite : Option (Nat × Nat) := - match compileFunctionNamed zeroAddressShadowSmokeSpec "shadowWrite" with - | .ok block => - match block.params with - | [shadowParam] => - let init : TExecState := - { world := Verity.defaultState - vars := { address := fun i => if i = shadowParam.id then 77 else 0 } } - match evalTBlock init block with - | .ok post => some (post.world.storageMap 0 77, post.world.storageMap 0 0) - | .revert _ => none - | _ => none - | _ => none - -/-- The `zeroAddress` builtin does not shadow user parameters during macro lowering. -/ -example : compiledZeroAddressShadowWrite = some (77, 0) := by native_decide - -/-- End-to-end ERC721 mint + approve + transferFrom success path. -/ -def compiledERC721ApproveTransferSuccess : Option (Nat × Nat × Nat × Nat) := - match compileFunctionNamed erc721Spec "mint", - compileFunctionNamed erc721Spec "approve", - compileFunctionNamed erc721Spec "transferFrom" with - | .ok mintBlock, .ok approveBlock, .ok transferBlock => - match mintBlock.params, approveBlock.params, transferBlock.params with - | [mintToParam], [approvedParam, approveTokenParam], [fromParam, toParam, transferTokenParam] => - let mintInitWorld : Verity.ContractState := - { Verity.defaultState.writeAddrSlot 0 9 with sender := 9 } - let mintInit : TExecState := - { world := mintInitWorld - vars := { address := fun i => if i = mintToParam.id then 11 else 0 } } - match evalTBlock mintInit mintBlock with - | .revert _ => none - | .ok postMint => - let approveInit : TExecState := - { world := { postMint.world with sender := 11 } - vars := { address := fun i => if i = approvedParam.id then 33 else 0, - uint256 := fun i => if i = approveTokenParam.id then 0 else 0 } } - match evalTBlock approveInit approveBlock with - | .revert _ => none - | .ok postApprove => - let transferInit : TExecState := - { world := { postApprove.world with sender := 33 } - vars := { address := fun i => if i = fromParam.id then 11 else if i = toParam.id then 33 else 0, - uint256 := fun i => if i = transferTokenParam.id then 0 else 0 } } - match evalTBlock transferInit transferBlock with - | .revert _ => none - | .ok s => - some (s.world.storageMapUint 4 0, s.world.storageMapUint 5 0, s.world.storage 1, - s.world.storage 2) - | _, _, _ => none - | _, _, _ => none - -/-- ERC721 tokenId 0 transfers owner 11→33; approval clears; supply=1 and nextTokenId=1. -/ -example : compiledERC721ApproveTransferSuccess = some (33, 0, 1, 1) := by native_decide - -/-- End-to-end ERC721.transferFrom unauthorized caller reverts. -/ -def compiledERC721TransferUnauthorizedReverts : Bool := - match compileFunctionNamed erc721Spec "mint", - compileFunctionNamed erc721Spec "transferFrom" with - | .ok mintBlock, .ok transferBlock => - match mintBlock.params, transferBlock.params with - | [mintToParam], [fromParam, toParam, transferTokenParam] => - let mintInitWorld : Verity.ContractState := - { Verity.defaultState.writeAddrSlot 0 9 with sender := 9 } - let mintInit : TExecState := - { world := mintInitWorld - vars := { address := fun i => if i = mintToParam.id then 11 else 0 } } - match evalTBlock mintInit mintBlock with - | .revert _ => false - | .ok postMint => - let transferInit : TExecState := - { world := { postMint.world with sender := 22 } - vars := { address := fun i => if i = fromParam.id then 11 else if i = toParam.id then 33 else 0, - uint256 := fun i => if i = transferTokenParam.id then 0 else 0 } } - match evalTBlock transferInit transferBlock with - | .ok _ => false - | .revert _ => true - | _, _ => false - | _, _ => false - -/-- ERC721.transferFrom without owner/approval authorization reverts. -/ -example : compiledERC721TransferUnauthorizedReverts = true := by native_decide - -/-- Smoke test: all SafeCounter spec functions compile to typed IR. -/ -example : compilesOk Contracts.SafeCounter.spec "increment" = true := by native_decide -example : compilesOk Contracts.SafeCounter.spec "decrement" = true := by native_decide -example : compilesOk Contracts.SafeCounter.spec "getCount" = true := by native_decide - -/-- End-to-end SafeCounter.increment (happy path): count 5 → 6. -/ -def compiledSafeCounterIncrementSuccess : Option Nat := - match compileFunctionNamed Contracts.SafeCounter.spec "increment" with - | .error _ => none - | .ok block => - let initWorld : Verity.ContractState := - Verity.defaultState.writeSlot 0 5 - let init : TExecState := { world := initWorld } - match evalTBlock init block with - | .ok s => some (s.world.storage 0) - | .revert _ => none - -/-- SafeCounter.increment(count=5): count 5→6. -/ -example : compiledSafeCounterIncrementSuccess = some 6 := by native_decide - -/-- End-to-end SafeCounter.decrement (happy path): count 5 → 4. -/ -def compiledSafeCounterDecrementSuccess : Option Nat := - match compileFunctionNamed Contracts.SafeCounter.spec "decrement" with - | .error _ => none - | .ok block => - let initWorld : Verity.ContractState := - Verity.defaultState.writeSlot 0 5 - let init : TExecState := { world := initWorld } - match evalTBlock init block with - | .ok s => some (s.world.storage 0) - | .revert _ => none - -/-- SafeCounter.decrement(count=5): count 5→4. -/ -example : compiledSafeCounterDecrementSuccess = some 4 := by native_decide - -/-- End-to-end SafeCounter.decrement (underflow): count=0 reverts. -/ -def compiledSafeCounterDecrementUnderflow : Bool := - match compileFunctionNamed Contracts.SafeCounter.spec "decrement" with - | .error _ => false - | .ok block => - let initWorld : Verity.ContractState := - Verity.defaultState - let init : TExecState := { world := initWorld } - match evalTBlock init block with - | .ok _ => false - | .revert _ => true - -/-- SafeCounter.decrement(count=0): reverts with "Underflow in decrement". -/ -example : compiledSafeCounterDecrementUnderflow = true := by native_decide - -/-- End-to-end SafeCounter.getCount: returns stored count via lowered pipeline. -/ -def compiledSafeCounterGetCountReturn : Option Nat := - match compileFunctionNamed Contracts.SafeCounter.spec "getCount" with - | .error _ => none - | .ok block => - let initWorld : Verity.ContractState := - Verity.defaultState.writeSlot 0 42 - let init : TExecState := { world := initWorld } - execLoweredReturn 256 (mkIRStateFromTyped init block) block - -/-- SafeCounter.getCount returns stored count (42) through the full pipeline. -/ -example : compiledSafeCounterGetCountReturn = some 42 := by native_decide - -/-- Compiler correctness base lemma: list compilation composes by append. -/ -example (fields : List Compiler.CompilationModel.Field) - (pre post : List Compiler.CompilationModel.Stmt) : - compileStmts fields (pre ++ post) = (do - compileStmts fields pre - compileStmts fields post) := - compileStmts_append fields pre post - -example (fields : List Compiler.CompilationModel.Field) - (pre post : List Compiler.CompilationModel.Stmt) - (st : CompileState) : - (compileStmts fields (pre ++ post)).run st = - ((do - compileStmts fields pre - compileStmts fields post).run st) := - compileStmts_append_from fields pre post st - -example (fields : List Compiler.CompilationModel.Field) - (fieldName : String) (slotIdx : Nat) - (init : TExecState) (n : Nat) - (hfind : Compiler.CompilationModel.findFieldWithResolvedSlot fields fieldName = - some (({ name := fieldName, ty := Compiler.CompilationModel.FieldType.uint256 } : Compiler.CompilationModel.Field), slotIdx)) : - execCompiledSetStorageLiteral fields fieldName init n = - .ok { init with world := execSourceSetStorageLiteral init.world slotIdx n } := - compile_setStorage_literal_semantics fields fieldName slotIdx init n hfind - -example (fields : List Compiler.CompilationModel.Field) - (fieldName tmp : String) (slotIdx : Nat) - (init : TExecState) (n : Nat) - (hfind : Compiler.CompilationModel.findFieldWithResolvedSlot fields fieldName = - some (({ name := fieldName, ty := Compiler.CompilationModel.FieldType.uint256 } : Compiler.CompilationModel.Field), slotIdx)) : - execCompiledLetSetStorageLocalLiteral fields fieldName tmp init n = - .ok - ({ init with - world := execSourceSetStorageLiteral init.world slotIdx n - vars := init.vars.set { id := 0, ty := Ty.uint256 } (n : Verity.Core.Uint256) }) := - compile_let_setStorage_local_literal_semantics fields fieldName tmp slotIdx init n hfind - -example (fields : List Compiler.CompilationModel.Field) - (fieldName tmp : String) (slotIdx : Nat) - (init : TExecState) (n m : Nat) - (hfind : Compiler.CompilationModel.findFieldWithResolvedSlot fields fieldName = - some (({ name := fieldName, ty := Compiler.CompilationModel.FieldType.uint256 } : Compiler.CompilationModel.Field), slotIdx)) : - execCompiledLetAssignSetStorageLocalLiteral fields fieldName tmp init n m = - .ok - ({ init with - world := execSourceSetStorageLiteral init.world slotIdx m - vars := TVars.set - (TVars.set init.vars { id := 0, ty := Ty.uint256 } (n : Verity.Core.Uint256)) - { id := 1, ty := Ty.uint256 } (m : Verity.Core.Uint256) }) := - compile_let_assign_setStorage_local_literal_semantics fields fieldName tmp slotIdx init n m hfind - -example (fields : List Compiler.CompilationModel.Field) - (fieldName tmp : String) (slotIdx : Nat) - (init : TExecState) (n m : Nat) - (hfind : Compiler.CompilationModel.findFieldWithResolvedSlot fields fieldName = - some (({ name := fieldName, ty := Compiler.CompilationModel.FieldType.uint256 } : Compiler.CompilationModel.Field), slotIdx)) : - execCompiledLetAssignAddSetStorageLocalLiteral fields fieldName tmp init n m = - .ok - ({ init with - world := execSourceSetStorageLiteral init.world slotIdx - ((n : Verity.Core.Uint256).add (m : Verity.Core.Uint256)) - vars := TVars.set - (TVars.set init.vars { id := 0, ty := Ty.uint256 } (n : Verity.Core.Uint256)) - { id := 1, ty := Ty.uint256 } - ((n : Verity.Core.Uint256).add (m : Verity.Core.Uint256)) }) := - compile_let_assign_add_setStorage_local_literal_semantics - fields fieldName tmp slotIdx init n m hfind - -example (fields : List Compiler.CompilationModel.Field) - (fieldName tmp : String) (slotIdx : Nat) - (init : TExecState) (n m : Nat) - (hfind : Compiler.CompilationModel.findFieldWithResolvedSlot fields fieldName = - some (({ name := fieldName, ty := Compiler.CompilationModel.FieldType.uint256 } : Compiler.CompilationModel.Field), slotIdx)) : - execCompiledLetAssignSubSetStorageLocalLiteral fields fieldName tmp init n m = - .ok - ({ init with - world := execSourceSetStorageLiteral init.world slotIdx - ((n : Verity.Core.Uint256).sub (m : Verity.Core.Uint256)) - vars := TVars.set - (TVars.set init.vars { id := 0, ty := Ty.uint256 } (n : Verity.Core.Uint256)) - { id := 1, ty := Ty.uint256 } - ((n : Verity.Core.Uint256).sub (m : Verity.Core.Uint256)) }) := - compile_let_assign_sub_setStorage_local_literal_semantics - fields fieldName tmp slotIdx init n m hfind - -example (fields : List Compiler.CompilationModel.Field) - (fieldName tmp : String) (slotIdx : Nat) - (init : TExecState) (n m : Nat) - (hfind : Compiler.CompilationModel.findFieldWithResolvedSlot fields fieldName = - some (({ name := fieldName, ty := Compiler.CompilationModel.FieldType.uint256 } : Compiler.CompilationModel.Field), slotIdx)) : - execCompiledLetAssignMulSetStorageLocalLiteral fields fieldName tmp init n m = - .ok - ({ init with - world := execSourceSetStorageLiteral init.world slotIdx - ((n : Verity.Core.Uint256).mul (m : Verity.Core.Uint256)) - vars := TVars.set - (TVars.set init.vars { id := 0, ty := Ty.uint256 } (n : Verity.Core.Uint256)) - { id := 1, ty := Ty.uint256 } - ((n : Verity.Core.Uint256).mul (m : Verity.Core.Uint256)) }) := - compile_let_assign_mul_setStorage_local_literal_semantics - fields fieldName tmp slotIdx init n m hfind - -example (fields : List Compiler.CompilationModel.Field) - (init : TExecState) (n : Nat) : - execCompiledReturnLiteral fields init n = execSourceReturnLiteral init n := - compile_return_literal_semantics fields init n - -example (fields : List Compiler.CompilationModel.Field) - (tmp : String) (init : TExecState) (n : Nat) : - execCompiledLetReturnLocalLiteral fields tmp init n = - execSourceLetReturnLocalLiteral init n := - compile_let_return_local_literal_semantics fields tmp init n - -example (fields : List Compiler.CompilationModel.Field) - (fieldName : String) (slotIdx : Nat) - (init : TExecState) (n m thenVal elseVal : Nat) - (hfind : Compiler.CompilationModel.findFieldWithResolvedSlot fields fieldName = - some (({ name := fieldName, ty := Compiler.CompilationModel.FieldType.uint256 } : Compiler.CompilationModel.Field), slotIdx)) : - execCompiledIteEqSetStorageLiterals fields fieldName init n m thenVal elseVal = - .ok { init with world := execSourceIteEqSetStorageLiterals init.world slotIdx n m thenVal elseVal } := - compile_ite_eq_setStorage_literals_semantics - fields fieldName slotIdx init n m thenVal elseVal hfind - -example (fields : List Compiler.CompilationModel.Field) - (init : TExecState) (n m : Nat) (message : String) : - execCompiledRequireEqLiterals fields init n m message = - execSourceRequireEqLiterals init n m message := - compile_require_eq_literals_semantics fields init n m message - -example (fields : List Compiler.CompilationModel.Field) - (init : TExecState) (n m : Nat) (message : String) : - execCompiledRequireNotEqLiterals fields init n m message = - execSourceRequireNotEqLiterals init n m message := - compile_require_not_eq_literals_semantics fields init n m message - -example (fields : List Compiler.CompilationModel.Field) - (init : TExecState) (n m : Nat) (message : String) : - execCompiledRequireLtLiterals fields init n m message = - execSourceRequireLtLiterals init n m message := - compile_require_lt_literals_semantics fields init n m message - -example (fields : List Compiler.CompilationModel.Field) - (init : TExecState) (n m : Nat) (message : String) : - execCompiledRequireGtLiterals fields init n m message = - execSourceRequireGtLiterals init n m message := - compile_require_gt_literals_semantics fields init n m message - -example (fields : List Compiler.CompilationModel.Field) - (init : TExecState) (n m : Nat) (message : String) : - execCompiledRequireGeLiterals fields init n m message = - execSourceRequireGeLiterals init n m message := - compile_require_ge_literals_semantics fields init n m message - -example (fields : List Compiler.CompilationModel.Field) - (init : TExecState) (n m : Nat) (message : String) : - execCompiledRequireLeLiterals fields init n m message = - execSourceRequireLeLiterals init n m message := - compile_require_le_literals_semantics fields init n m message - -example (guard : RequireBinaryLiteralGuard) - (fields : List Compiler.CompilationModel.Field) - (init : TExecState) (n m : Nat) (message : String) : - execCompiledRequireBinaryLiteralGuard guard fields init n m message = - execSourceRequireBinaryLiteralGuard guard init n m message := - compile_require_binary_literal_guard_semantics guard fields init n m message - -example (fields : List Compiler.CompilationModel.Field) - (init : TExecState) (clauses : List RequireBinaryLiteralClause) : - execCompiledRequireBinaryLiteralClauses fields init clauses = - execSourceRequireBinaryLiteralClauses init clauses := - compile_require_binary_literal_clauses_semantics fields init clauses - -example (fields : List Compiler.CompilationModel.Field) - (init : TExecState) (n m p q : Nat) (message : String) : - execCompiledRequireAndEqLtLiterals fields init n m p q message = - execSourceRequireAndEqLtLiterals init n m p q message := - compile_require_and_eq_lt_literals_semantics fields init n m p q message - -example (fields : List Compiler.CompilationModel.Field) - (init : TExecState) (n m p q : Nat) (message : String) : - execCompiledRequireOrEqLtLiterals fields init n m p q message = - execSourceRequireOrEqLtLiterals init n m p q message := - compile_require_or_eq_lt_literals_semantics fields init n m p q message - -example (family : RequireLiteralGuardFamily) - (fields : List Compiler.CompilationModel.Field) - (init : TExecState) (n m p q : Nat) (message : String) : - execCompiledRequireLiteralGuardFamily family fields init n m p q message = - execSourceRequireLiteralGuardFamily family init n m p q message := - compile_require_literal_guard_family_semantics family fields init n m p q message - -example (fields : List Compiler.CompilationModel.Field) - (init : TExecState) - (clauses : List RequireLiteralGuardFamilyClause) : - execCompiledRequireLiteralGuardFamilyClauses fields init clauses = - execSourceRequireLiteralGuardFamilyClauses init clauses := - compile_require_literal_guard_family_clauses_semantics fields init clauses - -example (fields : List Compiler.CompilationModel.Field) - (fieldName : String) (slotIdx : Nat) (init : TExecState) - (clauses : List RequireLiteralGuardFamilyClause) (writeVal : Nat) - (hfind : Compiler.CompilationModel.findFieldWithResolvedSlot fields fieldName = - some ({ name := fieldName, ty := Compiler.CompilationModel.FieldType.uint256 }, slotIdx)) : - execCompiledRequireFamilyClausesThenSetStorageLiteral - fields fieldName init clauses writeVal = - execSourceRequireFamilyClausesThenSetStorageLiteral - init clauses slotIdx writeVal := - compile_require_family_clauses_then_setStorage_literal_semantics - fields fieldName slotIdx init clauses writeVal hfind - -example (fields : List Compiler.CompilationModel.Field) - (init : TExecState) - (clauses : List RequireLiteralGuardFamilyClause) (retVal : Nat) : - execCompiledRequireFamilyClausesThenReturnLiteral - fields init clauses retVal = - execSourceRequireFamilyClausesThenReturnLiteral - init clauses retVal := - compile_require_family_clauses_then_return_literal_semantics - fields init clauses retVal - -example (fields : List Compiler.CompilationModel.Field) - (tmp : String) (init : TExecState) - (clauses : List RequireLiteralGuardFamilyClause) (retVal : Nat) : - execCompiledRequireFamilyClausesThenLetReturnLocalLiteral - fields tmp init clauses retVal = - execSourceRequireFamilyClausesThenLetReturnLocalLiteral - init clauses retVal := - compile_require_family_clauses_then_let_return_local_literal_semantics - fields tmp init clauses retVal - -example (fields : List Compiler.CompilationModel.Field) - (fieldName tmp : String) (slotIdx : Nat) (init : TExecState) - (clauses : List RequireLiteralGuardFamilyClause) (n : Nat) - (hfind : Compiler.CompilationModel.findFieldWithResolvedSlot fields fieldName = - some ({ name := fieldName, ty := Compiler.CompilationModel.FieldType.uint256 }, slotIdx)) : - execCompiledRequireFamilyClausesThenLetSetStorageLocalLiteral - fields fieldName tmp init clauses n = - execSourceRequireFamilyClausesThenLetSetStorageLocalLiteral - init clauses slotIdx n := - compile_require_family_clauses_then_let_setStorage_local_literal_semantics - fields fieldName tmp slotIdx init clauses n hfind - -example (fields : List Compiler.CompilationModel.Field) - (fieldName tmp : String) (slotIdx : Nat) (init : TExecState) - (clauses : List RequireLiteralGuardFamilyClause) (n m : Nat) - (hfind : Compiler.CompilationModel.findFieldWithResolvedSlot fields fieldName = - some ({ name := fieldName, ty := Compiler.CompilationModel.FieldType.uint256 }, slotIdx)) : - execCompiledRequireFamilyClausesThenLetAssignSetStorageLocalLiteral - fields fieldName tmp init clauses n m = - execSourceRequireFamilyClausesThenLetAssignSetStorageLocalLiteral - init clauses slotIdx n m := - compile_require_family_clauses_then_let_assign_setStorage_local_literal_semantics - fields fieldName tmp slotIdx init clauses n m hfind - -example (fields : List Compiler.CompilationModel.Field) - (fieldName tmp : String) (slotIdx : Nat) (init : TExecState) - (clauses : List RequireLiteralGuardFamilyClause) (n m : Nat) - (hfind : Compiler.CompilationModel.findFieldWithResolvedSlot fields fieldName = - some ({ name := fieldName, ty := Compiler.CompilationModel.FieldType.uint256 }, slotIdx)) : - execCompiledRequireFamilyClausesThenLetAssignAddSetStorageLocalLiteral - fields fieldName tmp init clauses n m = - execSourceRequireFamilyClausesThenLetAssignAddSetStorageLocalLiteral - init clauses slotIdx n m := - compile_require_family_clauses_then_let_assign_add_setStorage_local_literal_semantics - fields fieldName tmp slotIdx init clauses n m hfind - -example (fields : List Compiler.CompilationModel.Field) - (fieldName tmp : String) (slotIdx : Nat) (init : TExecState) - (clauses : List RequireLiteralGuardFamilyClause) (n m : Nat) - (hfind : Compiler.CompilationModel.findFieldWithResolvedSlot fields fieldName = - some ({ name := fieldName, ty := Compiler.CompilationModel.FieldType.uint256 }, slotIdx)) : - execCompiledRequireFamilyClausesThenLetAssignSubSetStorageLocalLiteral - fields fieldName tmp init clauses n m = - execSourceRequireFamilyClausesThenLetAssignSubSetStorageLocalLiteral - init clauses slotIdx n m := - compile_require_family_clauses_then_let_assign_sub_setStorage_local_literal_semantics - fields fieldName tmp slotIdx init clauses n m hfind - -example (fields : List Compiler.CompilationModel.Field) - (fieldName tmp : String) (slotIdx : Nat) (init : TExecState) - (clauses : List RequireLiteralGuardFamilyClause) (n m : Nat) - (hfind : Compiler.CompilationModel.findFieldWithResolvedSlot fields fieldName = - some ({ name := fieldName, ty := Compiler.CompilationModel.FieldType.uint256 }, slotIdx)) : - execCompiledRequireFamilyClausesThenLetAssignMulSetStorageLocalLiteral - fields fieldName tmp init clauses n m = - execSourceRequireFamilyClausesThenLetAssignMulSetStorageLocalLiteral - init clauses slotIdx n m := - compile_require_family_clauses_then_let_assign_mul_setStorage_local_literal_semantics - fields fieldName tmp slotIdx init clauses n m hfind - -example (family : RequireLiteralGuardFamily) - (fields : List Compiler.CompilationModel.Field) - (fieldName : String) (slotIdx : Nat) (init : TExecState) - (n m p q : Nat) (message : String) (writeVal : Nat) - (hfind : Compiler.CompilationModel.findFieldWithResolvedSlot fields fieldName = - some ({ name := fieldName, ty := Compiler.CompilationModel.FieldType.uint256 }, slotIdx)) : - execCompiledRequireFamilyThenSetStorageLiteral - family fields fieldName init n m p q message writeVal = - execSourceRequireFamilyThenSetStorageLiteral - family init slotIdx n m p q message writeVal := - compile_require_family_then_setStorage_literal_semantics - family fields fieldName slotIdx init n m p q message writeVal hfind - -example (family : RequireLiteralGuardFamily) - (fields : List Compiler.CompilationModel.Field) - (init : TExecState) - (n m p q : Nat) (message : String) (retVal : Nat) : - execCompiledRequireFamilyThenReturnLiteral - family fields init n m p q message retVal = - execSourceRequireFamilyThenReturnLiteral - family init n m p q message retVal := - compile_require_family_then_return_literal_semantics - family fields init n m p q message retVal - -example (fields : List Compiler.CompilationModel.Field) - (init : TExecState) - (clauses : List RequireLiteralGuardFamilyClause) : - execCompiledRequireFamilyClausesThenNoop fields init clauses = - execSourceRequireFamilyClausesThenNoop init clauses := - compile_require_family_clauses_then_noop_semantics fields init clauses - -example (fields : List Compiler.CompilationModel.Field) - (fieldName : String) (slotIdx : Nat) (init : TExecState) - (clauses : List RequireLiteralGuardFamilyClause) - (n m thenVal elseVal : Nat) - (hfind : Compiler.CompilationModel.findFieldWithResolvedSlot fields fieldName = - some ({ name := fieldName, ty := Compiler.CompilationModel.FieldType.uint256 }, slotIdx)) : - execCompiledRequireFamilyClausesThenIteEqSetStorageLiterals - fields fieldName init clauses n m thenVal elseVal = - execSourceRequireFamilyClausesThenIteEqSetStorageLiterals - init clauses slotIdx n m thenVal elseVal := - compile_require_family_clauses_then_ite_eq_setStorage_literals_semantics - fields fieldName slotIdx init clauses n m thenVal elseVal hfind - -example (fields : List Compiler.CompilationModel.Field) - (fieldName : String) (slotIdx : Nat) (init : TExecState) - (clauses : List RequireLiteralGuardFamilyClause) - (n m thenVal elseVal : Nat) - (hfind : Compiler.CompilationModel.findFieldWithResolvedSlot fields fieldName = - some ({ name := fieldName, ty := Compiler.CompilationModel.FieldType.uint256 }, slotIdx)) : - execCompiledRequireFamilyClausesThenIteEqSetStorageThenReturnLiteral - fields fieldName init clauses n m thenVal elseVal = - execSourceRequireFamilyClausesThenIteEqSetStorageThenReturnLiteral - init clauses slotIdx n m thenVal elseVal := - compile_require_family_clauses_then_ite_eq_setStorage_then_return_literal_semantics - fields fieldName slotIdx init clauses n m thenVal elseVal hfind - -example (fields : List Compiler.CompilationModel.Field) - (fieldName : String) (slotIdx : Nat) (init : TExecState) - (clauses : List RequireLiteralGuardFamilyClause) - (n m thenVal elseVal : Nat) - (hfind : Compiler.CompilationModel.findFieldWithResolvedSlot fields fieldName = - some ({ name := fieldName, ty := Compiler.CompilationModel.FieldType.uint256 }, slotIdx)) : - execCompiledRequireFamilyClausesThenIteEqReturnThenSetStorageLiteral - fields fieldName init clauses n m thenVal elseVal = - execSourceRequireFamilyClausesThenIteEqReturnThenSetStorageLiteral - init clauses slotIdx n m thenVal elseVal := - compile_require_family_clauses_then_ite_eq_return_then_setStorage_literal_semantics - fields fieldName slotIdx init clauses n m thenVal elseVal hfind - -example (fields : List Compiler.CompilationModel.Field) - (init : TExecState) - (clauses : List RequireLiteralGuardFamilyClause) - (n m thenVal elseVal : Nat) : - execCompiledRequireFamilyClausesThenIteEqReturnLiterals - fields init clauses n m thenVal elseVal = - execSourceRequireFamilyClausesThenIteEqReturnLiterals - init clauses n m thenVal elseVal := - compile_require_family_clauses_then_ite_eq_return_literals_semantics - fields init clauses n m thenVal elseVal - -example (fields : List Compiler.CompilationModel.Field) - (init : TExecState) - (clauses : List RequireLiteralGuardFamilyClause) - (n m p q thenVal elseVal outerElseVal : Nat) : - execCompiledRequireFamilyClausesThenIteEqThenIteEqReturnLiterals - fields init clauses n m p q thenVal elseVal outerElseVal = - execSourceRequireFamilyClausesThenIteEqThenIteEqReturnLiterals - init clauses n m p q thenVal elseVal outerElseVal := - compile_require_family_clauses_then_ite_eq_then_ite_eq_return_literals_semantics - fields init clauses n m p q thenVal elseVal outerElseVal - -example (fields : List Compiler.CompilationModel.Field) - (fieldName : String) (slotIdx : Nat) (init : TExecState) - (clauses : List RequireLiteralGuardFamilyClause) - (n m p q thenVal elseVal outerElseVal : Nat) - (hfind : Compiler.CompilationModel.findFieldWithResolvedSlot fields fieldName = - some ({ name := fieldName, ty := Compiler.CompilationModel.FieldType.uint256 }, slotIdx)) : - execCompiledRequireFamilyClausesThenIteEqThenIteEqSetStorageLiteralsThenReturnLiteral - fields fieldName init clauses n m p q thenVal elseVal outerElseVal = - execSourceRequireFamilyClausesThenIteEqThenIteEqSetStorageLiteralsThenReturnLiteral - init clauses slotIdx n m p q thenVal elseVal outerElseVal := - compile_require_family_clauses_then_ite_eq_then_ite_eq_setStorage_literals_then_return_literal_semantics - fields fieldName slotIdx init clauses n m p q thenVal elseVal outerElseVal hfind - -example (fields : List Compiler.CompilationModel.Field) - (fieldName : String) (slotIdx : Nat) (init : TExecState) - (clauses : List RequireLiteralGuardFamilyClause) - (n m p q thenVal elseVal outerElseVal : Nat) - (hfind : Compiler.CompilationModel.findFieldWithResolvedSlot fields fieldName = - some ({ name := fieldName, ty := Compiler.CompilationModel.FieldType.uint256 }, slotIdx)) : - execCompiledRequireFamilyClausesThenIteEqThenIteEqReturnLiteralsThenSetStorageLiteral - fields fieldName init clauses n m p q thenVal elseVal outerElseVal = - execSourceRequireFamilyClausesThenIteEqThenIteEqReturnLiteralsThenSetStorageLiteral - init clauses slotIdx n m p q thenVal elseVal outerElseVal := - compile_require_family_clauses_then_ite_eq_then_ite_eq_return_literals_then_setStorage_literal_semantics - fields fieldName slotIdx init clauses n m p q thenVal elseVal outerElseVal hfind - -example (fields : List Compiler.CompilationModel.Field) - (fieldName : String) (slotIdx : Nat) (init : TExecState) - (clauses : List RequireLiteralGuardFamilyClause) - (n m p q thenVal elseVal outerElseWriteVal outerElseRetVal : Nat) - (hfind : Compiler.CompilationModel.findFieldWithResolvedSlot fields fieldName = - some ({ name := fieldName, ty := Compiler.CompilationModel.FieldType.uint256 }, slotIdx)) : - execCompiledRequireFamilyClausesThenIteEqThenIteEqReturnLiteralsThenSetStorageLiteralThenReturnLiteral - fields fieldName init clauses n m p q thenVal elseVal outerElseWriteVal outerElseRetVal = - execSourceRequireFamilyClausesThenIteEqThenIteEqReturnLiteralsThenSetStorageLiteralThenReturnLiteral - init clauses slotIdx n m p q thenVal elseVal outerElseWriteVal outerElseRetVal := - compile_require_family_clauses_then_ite_eq_then_ite_eq_return_literals_then_setStorage_literal_then_return_literal_semantics - fields fieldName slotIdx init clauses n m p q thenVal elseVal outerElseWriteVal outerElseRetVal hfind - -example (fields : List Compiler.CompilationModel.Field) - (fieldName : String) (slotIdx : Nat) (init : TExecState) - (clauses : List RequireLiteralGuardFamilyClause) - (n m p q thenVal elseVal outerElseVal : Nat) - (hfind : Compiler.CompilationModel.findFieldWithResolvedSlot fields fieldName = - some ({ name := fieldName, ty := Compiler.CompilationModel.FieldType.uint256 }, slotIdx)) : - execCompiledRequireFamilyClausesThenIteEqThenIteEqSetStorageLiteralsThenSetStorageLiteral - fields fieldName init clauses n m p q thenVal elseVal outerElseVal = - execSourceRequireFamilyClausesThenIteEqThenIteEqSetStorageLiteralsThenSetStorageLiteral - init clauses slotIdx n m p q thenVal elseVal outerElseVal := - compile_require_family_clauses_then_ite_eq_then_ite_eq_setStorage_literals_then_setStorage_literal_semantics - fields fieldName slotIdx init clauses n m p q thenVal elseVal outerElseVal hfind - -example (fields : List Compiler.CompilationModel.Field) - (fieldName : String) (slotIdx : Nat) (init : TExecState) - (clauses : List RequireLiteralGuardFamilyClause) - (n m p q thenVal elseVal outerElseVal : Nat) - (hfind : Compiler.CompilationModel.findFieldWithResolvedSlot fields fieldName = - some ({ name := fieldName, ty := Compiler.CompilationModel.FieldType.uint256 }, slotIdx)) : - execCompiledRequireFamilyClausesThenIteEqThenIteEqSetStorageThenReturnLiteralThenReturnLiteral - fields fieldName init clauses n m p q thenVal elseVal outerElseVal = - execSourceRequireFamilyClausesThenIteEqThenIteEqSetStorageThenReturnLiteralThenReturnLiteral - init clauses slotIdx n m p q thenVal elseVal outerElseVal := - compile_require_family_clauses_then_ite_eq_then_ite_eq_setStorage_then_return_literal_then_return_literal_semantics - fields fieldName slotIdx init clauses n m p q thenVal elseVal outerElseVal hfind - -example (fields : List Compiler.CompilationModel.Field) - (fieldName : String) (slotIdx : Nat) (init : TExecState) - (clauses : List RequireLiteralGuardFamilyClause) - (n m p q thenVal elseVal outerElseVal : Nat) - (hfind : Compiler.CompilationModel.findFieldWithResolvedSlot fields fieldName = - some ({ name := fieldName, ty := Compiler.CompilationModel.FieldType.uint256 }, slotIdx)) : - execCompiledRequireFamilyClausesThenIteEqThenIteEqSetStorageThenReturnLiteralThenSetStorageLiteral - fields fieldName init clauses n m p q thenVal elseVal outerElseVal = - execSourceRequireFamilyClausesThenIteEqThenIteEqSetStorageThenReturnLiteralThenSetStorageLiteral - init clauses slotIdx n m p q thenVal elseVal outerElseVal := - compile_require_family_clauses_then_ite_eq_then_ite_eq_setStorage_then_return_literal_then_setStorage_literal_semantics - fields fieldName slotIdx init clauses n m p q thenVal elseVal outerElseVal hfind - -example (fields : List Compiler.CompilationModel.Field) - (fieldName : String) (slotIdx : Nat) (init : TExecState) - (clauses : List RequireLiteralGuardFamilyClause) - (n m p q thenVal elseVal outerElseVal : Nat) - (hfind : Compiler.CompilationModel.findFieldWithResolvedSlot fields fieldName = - some ({ name := fieldName, ty := Compiler.CompilationModel.FieldType.uint256 }, slotIdx)) : - execCompiledRequireFamilyClausesThenIteEqThenIteEqReturnThenSetStorageLiteralThenReturnLiteral - fields fieldName init clauses n m p q thenVal elseVal outerElseVal = - execSourceRequireFamilyClausesThenIteEqThenIteEqReturnThenSetStorageLiteralThenReturnLiteral - init clauses slotIdx n m p q thenVal elseVal outerElseVal := - compile_require_family_clauses_then_ite_eq_then_ite_eq_return_then_setStorage_literal_then_return_literal_semantics - fields fieldName slotIdx init clauses n m p q thenVal elseVal outerElseVal hfind - -example (fields : List Compiler.CompilationModel.Field) - (fieldName : String) (slotIdx : Nat) (init : TExecState) - (clauses : List RequireLiteralGuardFamilyClause) - (n m p q thenVal elseVal outerElseVal : Nat) - (hfind : Compiler.CompilationModel.findFieldWithResolvedSlot fields fieldName = - some ({ name := fieldName, ty := Compiler.CompilationModel.FieldType.uint256 }, slotIdx)) : - execCompiledRequireFamilyClausesThenIteEqThenIteEqReturnThenSetStorageLiteralThenSetStorageLiteral - fields fieldName init clauses n m p q thenVal elseVal outerElseVal = - execSourceRequireFamilyClausesThenIteEqThenIteEqReturnThenSetStorageLiteralThenSetStorageLiteral - init clauses slotIdx n m p q thenVal elseVal outerElseVal := - compile_require_family_clauses_then_ite_eq_then_ite_eq_return_then_setStorage_literal_then_setStorage_literal_semantics - fields fieldName slotIdx init clauses n m p q thenVal elseVal outerElseVal hfind - -example (fields : List Compiler.CompilationModel.Field) - (init : TExecState) - (clauses : List RequireLiteralGuardFamilyClause) - (tail : RequireFamilyClausesTail fields) : - execCompiledRequireFamilyClausesThenTail fields init clauses tail = - execSourceRequireFamilyClausesThenTail fields init clauses tail := - compile_require_family_clauses_then_tail_semantics fields init clauses tail - -example (fields : List Compiler.CompilationModel.Field) - (init : TExecState) - (programs : List (RequireFamilyClausesTailProgram fields)) : - execCompiledRequireFamilyClausesTailPrograms fields init programs = - execSourceRequireFamilyClausesTailPrograms fields init programs := - compile_require_family_clauses_tail_programs_semantics fields init programs - -example (fields : List Compiler.CompilationModel.Field) - (init : TExecState) - (pre post : List (RequireFamilyClausesTailProgram fields)) : - execCompiledRequireFamilyClausesTailPrograms fields init (pre ++ post) = - execSourceRequireFamilyClausesTailPrograms fields init (pre ++ post) := - compile_require_family_clauses_tail_programs_append_semantics fields init pre post - -example (fields : List Compiler.CompilationModel.Field) - (init : TExecState) - (fragments : List (SupportedStmtFragment fields)) : - execCompiledSupportedStmtFragments fields init fragments = - execSourceSupportedStmtFragments fields init fragments := - compile_supported_stmt_fragments_semantics fields init fragments - -example (fields : List Compiler.CompilationModel.Field) - (init : TExecState) - (stmts : List Compiler.CompilationModel.Stmt) - (hSupported : SupportedStmtList fields stmts) : - ∃ fragments : List (SupportedStmtFragment fields), - supportedStmtFragmentsToStmts fragments = stmts ∧ - execCompiledSupportedStmtFragments fields init fragments = - execSourceSupportedStmtFragments fields init fragments := - compile_supported_stmt_list_semantics fields init stmts hSupported - -example (fields : List Compiler.CompilationModel.Field) - (init : TExecState) - (stmts : List Compiler.CompilationModel.Stmt) - (hSupported : SupportedStmtList fields stmts) : - execCompiledSupportedStmtList fields init stmts hSupported = - execSourceSupportedStmtList fields init stmts hSupported := - compile_supported_stmt_list_direct_semantics fields init stmts hSupported - -open Compiler.CompilationModel in -/-- Counter.increment body belongs to the supported fragment. -/ -example : SupportedStmtList counterFields - [ Stmt.letVar "current" (Expr.storage "count") - , Stmt.setStorage "count" (Expr.add (Expr.localVar "current") (Expr.literal 1)) - , Stmt.stop ] := - counter_increment_supported - -open Compiler.CompilationModel in -/-- Counter.decrement body belongs to the supported fragment. -/ -example : SupportedStmtList counterFields - [ Stmt.letVar "current" (Expr.storage "count") - , Stmt.setStorage "count" (Expr.sub (Expr.localVar "current") (Expr.literal 1)) - , Stmt.stop ] := - counter_decrement_supported - -open Compiler.CompilationModel in -/-- Counter.getCount body belongs to the supported fragment. -/ -example : SupportedStmtList counterFields - [ Stmt.letVar "current" (Expr.storage "count") - , Stmt.return (Expr.localVar "current") ] := - counter_getCount_supported - -open Compiler.CompilationModel in -/-- Counter.increment compilation correctness follows from the generic theorem. -/ -example (init : TExecState) : - ∃ fragments : List (SupportedStmtFragment counterFields), - supportedStmtFragmentsToStmts fragments = - [ Stmt.letVar "current" (Expr.storage "count") - , Stmt.setStorage "count" (Expr.add (Expr.localVar "current") (Expr.literal 1)) - , Stmt.stop ] ∧ - execCompiledSupportedStmtFragments counterFields init fragments = - execSourceSupportedStmtFragments counterFields init fragments := - counter_increment_correctness init - -open Compiler.CompilationModel in -/-- Counter.decrement compilation correctness follows from the generic theorem. -/ -example (init : TExecState) : - ∃ fragments : List (SupportedStmtFragment counterFields), - supportedStmtFragmentsToStmts fragments = - [ Stmt.letVar "current" (Expr.storage "count") - , Stmt.setStorage "count" (Expr.sub (Expr.localVar "current") (Expr.literal 1)) - , Stmt.stop ] ∧ - execCompiledSupportedStmtFragments counterFields init fragments = - execSourceSupportedStmtFragments counterFields init fragments := - counter_decrement_correctness init - -open Compiler.CompilationModel in -/-- Counter.getCount compilation correctness follows from the generic theorem. -/ -example (init : TExecState) : - ∃ fragments : List (SupportedStmtFragment counterFields), - supportedStmtFragmentsToStmts fragments = - [ Stmt.letVar "current" (Expr.storage "count") - , Stmt.return (Expr.localVar "current") ] ∧ - execCompiledSupportedStmtFragments counterFields init fragments = - execSourceSupportedStmtFragments counterFields init fragments := - counter_getCount_correctness init - -open Compiler.CompilationModel in -/-- SimpleStorage.retrieve compilation correctness follows from the generic theorem. -/ -example (init : TExecState) : - ∃ fragments : List (SupportedStmtFragment simpleStorageFields), - supportedStmtFragmentsToStmts fragments = - [Stmt.return (Expr.storage "storedData")] ∧ - execCompiledSupportedStmtFragments simpleStorageFields init fragments = - execSourceSupportedStmtFragments simpleStorageFields init fragments := - simpleStorage_retrieve_correctness init - -open Compiler.CompilationModel in -/-- Owned.getOwner compilation correctness follows from the generic theorem. -/ -example (init : TExecState) : - ∃ fragments : List (SupportedStmtFragment ownedFields), - supportedStmtFragmentsToStmts fragments = - [Stmt.return (Expr.storage "owner")] ∧ - execCompiledSupportedStmtFragments ownedFields init fragments = - execSourceSupportedStmtFragments ownedFields init fragments := - owned_getOwner_correctness init - -open Compiler.CompilationModel in -/-- SafeCounter.getCount compilation correctness follows from the generic theorem. -/ -example (init : TExecState) : - ∃ fragments : List (SupportedStmtFragment safeCounterFields), - supportedStmtFragmentsToStmts fragments = - [Stmt.return (Expr.storage "count")] ∧ - execCompiledSupportedStmtFragments safeCounterFields init fragments = - execSourceSupportedStmtFragments safeCounterFields init fragments := - safeCounter_getCount_correctness init - -open Compiler.CompilationModel in -/-- OwnedCounter.getCount compilation correctness follows from the generic theorem. -/ -example (init : TExecState) : - ∃ fragments : List (SupportedStmtFragment ownedCounterFields), - supportedStmtFragmentsToStmts fragments = - [Stmt.return (Expr.storage "count")] ∧ - execCompiledSupportedStmtFragments ownedCounterFields init fragments = - execSourceSupportedStmtFragments ownedCounterFields init fragments := - ownedCounter_getCount_correctness init - -open Compiler.CompilationModel in -/-- OwnedCounter.getOwner compilation correctness follows from the generic theorem. -/ -example (init : TExecState) : - ∃ fragments : List (SupportedStmtFragment ownedCounterFields), - supportedStmtFragmentsToStmts fragments = - [Stmt.return (Expr.storage "owner")] ∧ - execCompiledSupportedStmtFragments ownedCounterFields init fragments = - execSourceSupportedStmtFragments ownedCounterFields init fragments := - ownedCounter_getOwner_correctness init - -open Compiler.CompilationModel in -/-- OwnedCounter.increment compilation correctness follows from the generic theorem. -/ -example (init : TExecState) : - ∃ fragments : List (SupportedStmtFragment ownedCounterFields), - supportedStmtFragmentsToStmts fragments = - [ Stmt.require (Expr.eq Expr.caller (Expr.storage "owner")) "Not owner" - , Stmt.setStorage "count" (Expr.add (Expr.storage "count") (Expr.literal 1)) - , Stmt.stop ] ∧ - execCompiledSupportedStmtFragments ownedCounterFields init fragments = - execSourceSupportedStmtFragments ownedCounterFields init fragments := - ownedCounter_increment_correctness init - -open Compiler.CompilationModel in -/-- OwnedCounter.decrement compilation correctness follows from the generic theorem. -/ -example (init : TExecState) : - ∃ fragments : List (SupportedStmtFragment ownedCounterFields), - supportedStmtFragmentsToStmts fragments = - [ Stmt.require (Expr.eq Expr.caller (Expr.storage "owner")) "Not owner" - , Stmt.setStorage "count" (Expr.sub (Expr.storage "count") (Expr.literal 1)) - , Stmt.stop ] ∧ - execCompiledSupportedStmtFragments ownedCounterFields init fragments = - execSourceSupportedStmtFragments ownedCounterFields init fragments := - ownedCounter_decrement_correctness init - --- ============================================================================ --- Mapping return pattern tests (item 3.5) --- ============================================================================ - -/-- End-to-end Ledger.getBalance: compiled block preserves state (return is read-only). -/ -def compiledLedgerGetBalancePreservesState : Bool := - match compileFunctionNamed Contracts.Ledger.spec "getBalance" with - | .error _ => false - | .ok block => - match block.params with - | [addrParam] => - let initWorld : Verity.ContractState := - Verity.defaultState.writeMap 0 42 200 - let init : TExecState := - { world := initWorld - vars := { address := fun i => - if i = addrParam.id then 42 else 0 } } - match evalTBlock init block with - | .ok _ => true - | .revert _ => false - | _ => false - -/-- Ledger.getBalance(addr=42): succeeds without reverting. -/ -example : compiledLedgerGetBalancePreservesState = true := by native_decide - -/-- End-to-end SimpleToken.balanceOf: compiled block preserves state (return is read-only). -/ -def compiledSimpleTokenBalanceOfPreservesState : Bool := - match compileFunctionNamed Contracts.SimpleToken.spec "balanceOf" with - | .error _ => false - | .ok block => - match block.params with - | [addrParam] => - let initWorld : Verity.ContractState := - Verity.defaultState.writeMap 1 42 500 - let init : TExecState := - { world := initWorld - vars := { address := fun i => - if i = addrParam.id then 42 else 0 } } - match evalTBlock init block with - | .ok _ => true - | .revert _ => false - | _ => false - -/-- SimpleToken.balanceOf(addr=42): succeeds without reverting. -/ -example : compiledSimpleTokenBalanceOfPreservesState = true := by native_decide - -/-- End-to-end SimpleToken.totalSupply: compiled block preserves state (return is read-only). -/ -def compiledSimpleTokenTotalSupplyPreservesState : Bool := - match compileFunctionNamed Contracts.SimpleToken.spec "totalSupply" with - | .error _ => false - | .ok block => - let initWorld : Verity.ContractState := - Verity.defaultState.writeSlot 2 1000 - let init : TExecState := { world := initWorld } - match evalTBlock init block with - | .ok _ => true - | .revert _ => false - -/-- SimpleToken.totalSupply: succeeds without reverting. -/ -example : compiledSimpleTokenTotalSupplyPreservesState = true := by native_decide - -/-- End-to-end SimpleToken.owner: compiled block preserves state (return is read-only). -/ -def compiledSimpleTokenOwnerPreservesState : Bool := - match compileFunctionNamed Contracts.SimpleToken.spec "owner" with - | .error _ => false - | .ok block => - let initWorld : Verity.ContractState := - Verity.defaultState.writeAddrSlot 0 42 - let init : TExecState := { world := initWorld } - match evalTBlock init block with - | .ok _ => true - | .revert _ => false - -/-- SimpleToken.owner: succeeds without reverting. -/ -example : compiledSimpleTokenOwnerPreservesState = true := by native_decide - -open Compiler.CompilationModel in -/-- SimpleToken.totalSupply compilation correctness follows from the generic theorem. -/ -example (init : TExecState) : - ∃ fragments : List (SupportedStmtFragment simpleTokenFields), - supportedStmtFragmentsToStmts fragments = - [Stmt.return (Expr.storage "totalSupply")] ∧ - execCompiledSupportedStmtFragments simpleTokenFields init fragments = - execSourceSupportedStmtFragments simpleTokenFields init fragments := - simpleToken_totalSupply_correctness init - -open Compiler.CompilationModel in -/-- SimpleToken.owner compilation correctness follows from the generic theorem. -/ -example (init : TExecState) : - ∃ fragments : List (SupportedStmtFragment simpleTokenFields), - supportedStmtFragmentsToStmts fragments = - [Stmt.return (Expr.storage "owner")] ∧ - execCompiledSupportedStmtFragments simpleTokenFields init fragments = - execSourceSupportedStmtFragments simpleTokenFields init fragments := - simpleToken_owner_correctness init - -open Compiler.CompilationModel in -/-- Timestamp-write + stop pattern belongs to the supported fragment grammar. -/ -example : SupportedStmtList simpleTokenFields - [ Stmt.letVar "ts" Expr.blockTimestamp - , Stmt.setStorage "totalSupply" (Expr.localVar "ts") - , Stmt.stop ] := - witness_requireClausesThenLetBlockTimestampSetStorageLocalStop_supported - -open Compiler.CompilationModel in -/-- Timestamp-write + stop correctness follows from the supported-list theorem. -/ -example (init : TExecState) : - ∃ fragments : List (SupportedStmtFragment simpleTokenFields), - supportedStmtFragmentsToStmts fragments = - [ Stmt.letVar "ts" Expr.blockTimestamp - , Stmt.setStorage "totalSupply" (Expr.localVar "ts") - , Stmt.stop ] ∧ - execCompiledSupportedStmtFragments simpleTokenFields init fragments = - execSourceSupportedStmtFragments simpleTokenFields init fragments := - compile_supported_stmt_list_semantics simpleTokenFields init _ - witness_requireClausesThenLetBlockTimestampSetStorageLocalStop_supported - -open Compiler.CompilationModel in -/-- Literal raw-log pattern belongs to the supported fragment grammar. -/ -example : SupportedStmtList simpleTokenFields - [Stmt.rawLog [Expr.literal 1, Expr.literal 2] (Expr.literal 0) (Expr.literal 64)] := - witness_requireClausesThenRawLogLiterals_supported - -open Compiler.CompilationModel in -/-- Literal raw-log correctness follows from the supported-list theorem. -/ -example (init : TExecState) : - ∃ fragments : List (SupportedStmtFragment simpleTokenFields), - supportedStmtFragmentsToStmts fragments = - [Stmt.rawLog [Expr.literal 1, Expr.literal 2] (Expr.literal 0) (Expr.literal 64)] ∧ - execCompiledSupportedStmtFragments simpleTokenFields init fragments = - execSourceSupportedStmtFragments simpleTokenFields init fragments := - compile_supported_stmt_list_semantics simpleTokenFields init _ - witness_requireClausesThenRawLogLiterals_supported - -open Compiler.CompilationModel in -/-- Four-topic raw-log boundary pattern is admitted by the supported grammar. -/ -example : SupportedStmtList simpleTokenFields - [Stmt.rawLog [Expr.literal 1, Expr.literal 2, Expr.literal 3, Expr.literal 4] - (Expr.literal 0) (Expr.literal 64)] := by - refine ⟨[.requireClausesThenRawLogLiterals [] [1, 2, 3, 4] 0 64 (by decide)], rfl⟩ - -open Compiler.CompilationModel in -/-- Four-topic raw-log boundary correctness follows from the supported-list theorem. -/ -example (init : TExecState) : - ∃ fragments : List (SupportedStmtFragment simpleTokenFields), - supportedStmtFragmentsToStmts fragments = - [Stmt.rawLog [Expr.literal 1, Expr.literal 2, Expr.literal 3, Expr.literal 4] - (Expr.literal 0) (Expr.literal 64)] ∧ - execCompiledSupportedStmtFragments simpleTokenFields init fragments = - execSourceSupportedStmtFragments simpleTokenFields init fragments := by - refine compile_supported_stmt_list_semantics simpleTokenFields init _ ?_ - refine ⟨[.requireClausesThenRawLogLiterals [] [1, 2, 3, 4] 0 64 (by decide)], rfl⟩ - -open Compiler.CompilationModel in -/-- Literal emit pattern belongs to the supported fragment grammar. -/ -example : SupportedStmtList simpleTokenFields - [Stmt.emit "Transfer" [Expr.literal 1, Expr.literal 2, Expr.literal 3]] := - witness_requireClausesThenEmitLiterals_supported - -open Compiler.CompilationModel in -/-- Literal emit correctness follows from the supported-list theorem. -/ -example (init : TExecState) : - ∃ fragments : List (SupportedStmtFragment simpleTokenFields), - supportedStmtFragmentsToStmts fragments = - [Stmt.emit "Transfer" [Expr.literal 1, Expr.literal 2, Expr.literal 3]] ∧ - execCompiledSupportedStmtFragments simpleTokenFields init fragments = - execSourceSupportedStmtFragments simpleTokenFields init fragments := - compile_supported_stmt_list_semantics simpleTokenFields init _ - witness_requireClausesThenEmitLiterals_supported - -open Compiler.CompilationModel in -/-- Three-arg emit boundary pattern is admitted by the supported grammar. -/ -example : SupportedStmtList simpleTokenFields - [Stmt.emit "Approval" [Expr.literal 11, Expr.literal 22, Expr.literal 33]] := by - refine ⟨[.requireClausesThenEmitLiterals [] "Approval" [11, 22, 33] (by decide)], rfl⟩ - -open Compiler.CompilationModel in -/-- Mapping return semantics preservation: compiled matches source for `return (mapping field caller)`. -/ -example (fields : List Field) - (fieldName : String) (slotIdx : Nat) - (init : TExecState) - (hSlot : findFieldSlot fields fieldName = some slotIdx) : - execCompiledReturnMappingCaller fields fieldName init = - execSourceReturnMappingCaller init slotIdx := - compile_return_mapping_caller_semantics fields fieldName slotIdx init hSlot - --- ============================================================================ --- ERC20 compilation tests --- ============================================================================ - -/-- End-to-end ERC20.totalSupply: compiled block executes without reverting. -/ -def compiledERC20TotalSupplyPreservesState : Bool := - match compileFunctionNamed Contracts.ERC20.spec "totalSupply" with - | .error _ => false - | .ok block => - let initWorld : Verity.ContractState := - Verity.defaultState.writeSlot 1 42000 - let init : TExecState := { world := initWorld } - match evalTBlock init block with - | .ok _ => true - | .revert _ => false - -/-- ERC20.totalSupply: succeeds without reverting. -/ -example : compiledERC20TotalSupplyPreservesState = true := by native_decide - -/-- End-to-end ERC20.owner: compiled block executes without reverting. -/ -def compiledERC20OwnerPreservesState : Bool := - match compileFunctionNamed Contracts.ERC20.spec "owner" with - | .error _ => false - | .ok block => - let initWorld : Verity.ContractState := - Verity.defaultState.writeAddrSlot 0 0xDEAD - let init : TExecState := { world := initWorld } - match evalTBlock init block with - | .ok _ => true - | .revert _ => false - -/-- ERC20.owner: succeeds without reverting. -/ -example : compiledERC20OwnerPreservesState = true := by native_decide - -/-- End-to-end ERC20.balanceOf: compiled block executes without reverting. -/ -def compiledERC20BalanceOfPreservesState : Bool := - match compileFunctionNamed Contracts.ERC20.spec "balanceOf" with - | .error _ => false - | .ok block => - match block.params with - | [addrParam] => - let initWorld : Verity.ContractState := - Verity.defaultState.writeMap 2 42 500 - let init : TExecState := - { world := initWorld - vars := { address := fun i => - if i = addrParam.id then 42 else 0 } } - match evalTBlock init block with - | .ok _ => true - | .revert _ => false - | _ => false - -/-- ERC20.balanceOf: succeeds without reverting. -/ -example : compiledERC20BalanceOfPreservesState = true := by native_decide - -/-- End-to-end ERC20.allowanceOf: compiled block executes without reverting. -/ -def compiledERC20AllowanceOfPreservesState : Bool := - match compileFunctionNamed Contracts.ERC20.spec "allowanceOf" with - | .error _ => false - | .ok block => - match block.params with - | [ownerParam, spenderParam] => - let initWorld : Verity.ContractState := - Verity.defaultState.writeMap2 3 100 200 750 - let init : TExecState := - { world := initWorld - vars := { address := fun i => - if i = ownerParam.id then 100 - else if i = spenderParam.id then 200 - else 0 } } - match evalTBlock init block with - | .ok _ => true - | .revert _ => false - | _ => false - -/-- ERC20.allowanceOf: succeeds without reverting. -/ -example : compiledERC20AllowanceOfPreservesState = true := by native_decide - -/-- End-to-end ERC20.approve: compiled block executes without reverting. -/ -def compiledERC20ApprovePreservesState : Bool := - match compileFunctionNamed Contracts.ERC20.spec "approve" with - | .error _ => false - | .ok block => - match block.params with - | [spenderParam, amountParam] => - let initWorld : Verity.ContractState := - Verity.defaultState - let init : TExecState := - { world := initWorld - env := { Verity.Env.ofWorld initWorld with sender := 0xCAFE } - vars := { address := fun i => - if i = spenderParam.id then 300 else 0 - uint256 := fun i => - if i = amountParam.id then 999 else 0 } } - match evalTBlock init block with - | .ok _ => true - | .revert _ => false - | _ => false - -/-- ERC20.approve: succeeds without reverting. -/ -example : compiledERC20ApprovePreservesState = true := by native_decide - -open Compiler.CompilationModel in -/-- ERC20.totalSupply correctness instantiation. -/ -example (init : TExecState) : - ∃ fragments : List (SupportedStmtFragment erc20Fields), - supportedStmtFragmentsToStmts fragments = - [ Stmt.letVar "currentSupply" (Expr.storage "totalSupplySlot") - , Stmt.return (Expr.localVar "currentSupply") ] ∧ - execCompiledSupportedStmtFragments erc20Fields init fragments = - execSourceSupportedStmtFragments erc20Fields init fragments := - erc20_totalSupply_correctness init - -open Compiler.CompilationModel in -/-- `SimpleToken.mint`: owner path keeps compiled/source multi-read outputs aligned. -/ -private def simpleTokenMintOwnerAgreement : Bool := - let init : TExecState := - { world := ((((Verity.defaultState.writeAddrSlot 0 0xA11CE).writeMap 1 0xBEEF 7).writeMap - 1 0xF00D 99).writeSlot 2 100).writeSlot 9 444 - env := { Verity.Env.ofWorld Verity.defaultState with sender := 0xA11CE } - vars := { address := fun i => if i = 0 then 0xBEEF else 0 - uint256 := fun i => if i = 1 then 5 else 0 } } - match - execCompiledLetCallerLetStorageAddrReqEqLetMappingLetStorageSetMappingAddParamSetStorageAddParamStop - simpleTokenFields "owner" "balances" "totalSupply" - "sender" "currentOwner" "currentBalance" "currentSupply" "to" "amount" - "Caller is not the owner" init, - execSourceLetCallerLetStorageAddrReqEqLetMappingLetStorageSetMappingAddParamSetStorageAddParamStop - init 0 1 2 "Caller is not the owner" with - | .ok c, .ok s => - c.world.storageMap 1 0xBEEF = 12 && - c.world.storage 2 = 105 && - c.world.storageMap 1 0xF00D = 99 && - c.world.storage 9 = 444 && - c.world.storageAddr 0 = 0xA11CE && - c.vars.address 2 = 0xA11CE && - c.vars.address 3 = 0xA11CE && - c.vars.uint256 4 = 7 && - c.vars.uint256 5 = 100 && - c.world.storageMap 1 0xBEEF = s.world.storageMap 1 0xBEEF && - c.world.storage 2 = s.world.storage 2 && - c.world.storageMap 1 0xF00D = s.world.storageMap 1 0xF00D && - c.world.storage 9 = s.world.storage 9 && - c.world.storageAddr 0 = s.world.storageAddr 0 && - c.vars.address 2 = s.vars.address 2 && - c.vars.address 3 = s.vars.address 3 && - c.vars.uint256 4 = s.vars.uint256 4 && - c.vars.uint256 5 = s.vars.uint256 5 - | _, _ => false - -example : simpleTokenMintOwnerAgreement = true := by - native_decide - -open Compiler.CompilationModel in -/-- `SimpleToken.mint`: generic owner-auth multi-read theorem instantiates cleanly. -/ -example (init : TExecState) : - execCompiledLetCallerLetStorageAddrReqEqLetMappingLetStorageSetMappingAddParamSetStorageAddParamStop - simpleTokenFields "owner" "balances" "totalSupply" - "sender" "currentOwner" "currentBalance" "currentSupply" "to" "amount" - "Caller is not the owner" init = - execSourceLetCallerLetStorageAddrReqEqLetMappingLetStorageSetMappingAddParamSetStorageAddParamStop - init 0 1 2 "Caller is not the owner" := - simpleToken_mint_ownerAuth_multiRead_correctness init - -open Compiler.CompilationModel in -/-- `SimpleToken.mint`: non-owner path reverts identically. -/ -private def simpleTokenMintNonOwnerAgreement : Bool := - let init : TExecState := - { world := ((Verity.defaultState.writeAddrSlot 0 0xA11CE).writeMap 1 0xBEEF 7).writeSlot 2 100 - env := { Verity.Env.ofWorld Verity.defaultState with sender := 0xBAD } - vars := { address := fun i => if i = 0 then 0xBEEF else 0 - uint256 := fun i => if i = 1 then 5 else 0 } } - match - execCompiledLetCallerLetStorageAddrReqEqLetMappingLetStorageSetMappingAddParamSetStorageAddParamStop - simpleTokenFields "owner" "balances" "totalSupply" - "sender" "currentOwner" "currentBalance" "currentSupply" "to" "amount" - "Caller is not the owner" init, - execSourceLetCallerLetStorageAddrReqEqLetMappingLetStorageSetMappingAddParamSetStorageAddParamStop - init 0 1 2 "Caller is not the owner" with - | .revert c, .revert s => c = "Caller is not the owner" && c = s - | _, _ => false - -example : simpleTokenMintNonOwnerAgreement = true := by - native_decide - -open Compiler.CompilationModel in -/-- Admin `setOwner` pattern belongs to the supported fragment grammar. -/ -example : SupportedStmtList adminPatternFields - [ Stmt.letVar "sender" Expr.caller - , Stmt.letVar "ownerVar" (Expr.storage "owner") - , Stmt.require (Expr.eq (Expr.localVar "sender") (Expr.localVar "ownerVar")) "not owner" - , Stmt.require (Expr.logicalNot (Expr.eq (Expr.param "newOwner") (Expr.localVar "ownerVar"))) - "already owner" - , Stmt.setStorage "owner" (Expr.param "newOwner") - , Stmt.stop ] := - witness_letCallerLetStorageAddrReqEqReqNeqSetStorageAddrParamStop_owner_supported - -open Compiler.CompilationModel in -/-- Admin `setFeeRecipient` pattern belongs to the supported fragment grammar. -/ -example : SupportedStmtList adminPatternFields - [ Stmt.letVar "sender" Expr.caller - , Stmt.letVar "currentFeeRecipient" (Expr.storage "feeRecipient") - , Stmt.require (Expr.eq (Expr.localVar "sender") (Expr.localVar "currentFeeRecipient")) - "not fee recipient" - , Stmt.require - (Expr.logicalNot (Expr.eq (Expr.param "newFeeRecipient") (Expr.localVar "currentFeeRecipient"))) - "already fee recipient" - , Stmt.setStorage "feeRecipient" (Expr.param "newFeeRecipient") - , Stmt.stop ] := - witness_letCallerLetStorageAddrReqEqReqNeqSetStorageAddrParamStop_feeRecipient_supported - -open Compiler.CompilationModel in -/-- Owner-auth `setFeeRecipient` pattern belongs to the supported fragment grammar. -/ -example : SupportedStmtList adminPatternFields - [ Stmt.letVar "sender" Expr.caller - , Stmt.letVar "ownerVar" (Expr.storage "owner") - , Stmt.require (Expr.eq (Expr.localVar "sender") (Expr.localVar "ownerVar")) "not owner" - , Stmt.letVar "currentFeeRecipient" (Expr.storage "feeRecipient") - , Stmt.require - (Expr.logicalNot (Expr.eq (Expr.param "newFeeRecipient") (Expr.localVar "currentFeeRecipient"))) - "already fee recipient" - , Stmt.setStorage "feeRecipient" (Expr.param "newFeeRecipient") - , Stmt.stop ] := - witness_letCallerLetStorageAddrReqEqLetStorageAddrReqNeqSetStorageAddrParamStop_feeRecipient_supported - -open Compiler.CompilationModel in -/-- `setOwner` supported-list compilation correctness instantiation. -/ -example (init : TExecState) : - ∃ fragments : List (SupportedStmtFragment adminPatternFields), - supportedStmtFragmentsToStmts fragments = - [ Stmt.letVar "sender" Expr.caller - , Stmt.letVar "ownerVar" (Expr.storage "owner") - , Stmt.require (Expr.eq (Expr.localVar "sender") (Expr.localVar "ownerVar")) "not owner" - , Stmt.require (Expr.logicalNot (Expr.eq (Expr.param "newOwner") (Expr.localVar "ownerVar"))) - "already owner" - , Stmt.setStorage "owner" (Expr.param "newOwner") - , Stmt.stop ] ∧ - execCompiledSupportedStmtFragments adminPatternFields init fragments = - execSourceSupportedStmtFragments adminPatternFields init fragments := - compile_supported_stmt_list_semantics adminPatternFields init _ - witness_letCallerLetStorageAddrReqEqReqNeqSetStorageAddrParamStop_owner_supported - -open Compiler.CompilationModel in -/-- `setFeeRecipient` supported-list compilation correctness instantiation. -/ -example (init : TExecState) : - ∃ fragments : List (SupportedStmtFragment adminPatternFields), - supportedStmtFragmentsToStmts fragments = - [ Stmt.letVar "sender" Expr.caller - , Stmt.letVar "currentFeeRecipient" (Expr.storage "feeRecipient") - , Stmt.require (Expr.eq (Expr.localVar "sender") (Expr.localVar "currentFeeRecipient")) - "not fee recipient" - , Stmt.require - (Expr.logicalNot (Expr.eq (Expr.param "newFeeRecipient") (Expr.localVar "currentFeeRecipient"))) - "already fee recipient" - , Stmt.setStorage "feeRecipient" (Expr.param "newFeeRecipient") - , Stmt.stop ] ∧ - execCompiledSupportedStmtFragments adminPatternFields init fragments = - execSourceSupportedStmtFragments adminPatternFields init fragments := - compile_supported_stmt_list_semantics adminPatternFields init _ - witness_letCallerLetStorageAddrReqEqReqNeqSetStorageAddrParamStop_feeRecipient_supported - -open Compiler.CompilationModel in -/-- Owner-auth `setFeeRecipient` supported-list compilation correctness instantiation. -/ -example (init : TExecState) : - ∃ fragments : List (SupportedStmtFragment adminPatternFields), - supportedStmtFragmentsToStmts fragments = - [ Stmt.letVar "sender" Expr.caller - , Stmt.letVar "ownerVar" (Expr.storage "owner") - , Stmt.require (Expr.eq (Expr.localVar "sender") (Expr.localVar "ownerVar")) "not owner" - , Stmt.letVar "currentFeeRecipient" (Expr.storage "feeRecipient") - , Stmt.require - (Expr.logicalNot (Expr.eq (Expr.param "newFeeRecipient") (Expr.localVar "currentFeeRecipient"))) - "already fee recipient" - , Stmt.setStorage "feeRecipient" (Expr.param "newFeeRecipient") - , Stmt.stop ] ∧ - execCompiledSupportedStmtFragments adminPatternFields init fragments = - execSourceSupportedStmtFragments adminPatternFields init fragments := - compile_supported_stmt_list_semantics adminPatternFields init _ - witness_letCallerLetStorageAddrReqEqLetStorageAddrReqNeqSetStorageAddrParamStop_feeRecipient_supported - -open Compiler.CompilationModel in -/-- ERC20.owner correctness instantiation. -/ -example (init : TExecState) : - ∃ fragments : List (SupportedStmtFragment erc20Fields), - supportedStmtFragmentsToStmts fragments = - [ Stmt.letVar "currentOwner" (Expr.storage "ownerSlot") - , Stmt.return (Expr.localVar "currentOwner") ] ∧ - execCompiledSupportedStmtFragments erc20Fields init fragments = - execSourceSupportedStmtFragments erc20Fields init fragments := - erc20_owner_correctness init - -open Compiler.CompilationModel in -/-- ERC20.balanceOf correctness instantiation. -/ -example (init : TExecState) : - ∃ fragments : List (SupportedStmtFragment erc20Fields), - supportedStmtFragmentsToStmts fragments = - [ Stmt.letVar "currentBalance" (Expr.mapping "balancesSlot" (Expr.param "addr")) - , Stmt.return (Expr.localVar "currentBalance") ] ∧ - execCompiledSupportedStmtFragments erc20Fields init fragments = - execSourceSupportedStmtFragments erc20Fields init fragments := - erc20_balanceOf_correctness init - -open Compiler.CompilationModel in -/-- ERC20.allowanceOf correctness instantiation. -/ -example (init : TExecState) : - ∃ fragments : List (SupportedStmtFragment erc20Fields), - supportedStmtFragmentsToStmts fragments = - [ Stmt.letVar "currentAllowance" - (Expr.mapping2 "allowancesSlot" (Expr.param "ownerAddr") (Expr.param "spender")) - , Stmt.return (Expr.localVar "currentAllowance") ] ∧ - execCompiledSupportedStmtFragments erc20Fields init fragments = - execSourceSupportedStmtFragments erc20Fields init fragments := - erc20_allowanceOf_correctness init - -open Compiler.CompilationModel in -/-- ERC20.approve correctness instantiation. -/ -example (init : TExecState) : - ∃ fragments : List (SupportedStmtFragment erc20Fields), - supportedStmtFragmentsToStmts fragments = - [ Stmt.letVar "sender" Expr.caller - , Stmt.setMapping2 "allowancesSlot" (Expr.localVar "sender") - (Expr.param "spender") (Expr.param "amount") - , Stmt.stop ] ∧ - execCompiledSupportedStmtFragments erc20Fields init fragments = - execSourceSupportedStmtFragments erc20Fields init fragments := - erc20_approve_correctness init - -open Compiler.CompilationModel in -/-- `ERC20.mint`: owner path keeps compiled/source multi-read outputs aligned. -/ -private def erc20MintOwnerAgreement : Bool := - let init : TExecState := - { world := ((((Verity.defaultState.writeAddrSlot 0 0xCAFE).writeMap 2 0xD00D 11).writeMap - 2 0xABCD 77).writeSlot 1 250).writeSlot 8 333 - env := { Verity.Env.ofWorld Verity.defaultState with sender := 0xCAFE } - vars := { address := fun i => if i = 0 then 0xD00D else 0 - uint256 := fun i => if i = 1 then 9 else 0 } } - match - execCompiledLetCallerLetStorageAddrReqEqLetMappingLetStorageSetMappingAddParamSetStorageAddParamStop - erc20Fields "ownerSlot" "balancesSlot" "totalSupplySlot" - "sender" "currentOwner" "currentBalance" "currentSupply" "to" "amount" - "Caller is not the owner" init, - execSourceLetCallerLetStorageAddrReqEqLetMappingLetStorageSetMappingAddParamSetStorageAddParamStop - init 0 2 1 "Caller is not the owner" with - | .ok c, .ok s => - c.world.storageMap 2 0xD00D = 20 && - c.world.storage 1 = 259 && - c.world.storageMap 2 0xABCD = 77 && - c.world.storage 8 = 333 && - c.world.storageAddr 0 = 0xCAFE && - c.vars.address 2 = 0xCAFE && - c.vars.address 3 = 0xCAFE && - c.vars.uint256 4 = 11 && - c.vars.uint256 5 = 250 && - c.world.storageMap 2 0xD00D = s.world.storageMap 2 0xD00D && - c.world.storage 1 = s.world.storage 1 && - c.world.storageMap 2 0xABCD = s.world.storageMap 2 0xABCD && - c.world.storage 8 = s.world.storage 8 && - c.world.storageAddr 0 = s.world.storageAddr 0 && - c.vars.address 2 = s.vars.address 2 && - c.vars.address 3 = s.vars.address 3 && - c.vars.uint256 4 = s.vars.uint256 4 && - c.vars.uint256 5 = s.vars.uint256 5 - | _, _ => false - -example : erc20MintOwnerAgreement = true := by - native_decide - -open Compiler.CompilationModel in -/-- `ERC20.mint`: generic owner-auth multi-read theorem instantiates cleanly. -/ -example (init : TExecState) : - execCompiledLetCallerLetStorageAddrReqEqLetMappingLetStorageSetMappingAddParamSetStorageAddParamStop - erc20Fields "ownerSlot" "balancesSlot" "totalSupplySlot" - "sender" "currentOwner" "currentBalance" "currentSupply" "to" "amount" - "Caller is not the owner" init = - execSourceLetCallerLetStorageAddrReqEqLetMappingLetStorageSetMappingAddParamSetStorageAddParamStop - init 0 2 1 "Caller is not the owner" := - erc20_mint_ownerAuth_multiRead_correctness init - -open Compiler.CompilationModel in -/-- `ERC20.mint`: non-owner path reverts identically. -/ -private def erc20MintNonOwnerAgreement : Bool := - let init : TExecState := - { world := ((Verity.defaultState.writeAddrSlot 0 0xCAFE).writeMap 2 0xD00D 11).writeSlot 1 250 - env := { Verity.Env.ofWorld Verity.defaultState with sender := 0xBAD } - vars := { address := fun i => if i = 0 then 0xD00D else 0 - uint256 := fun i => if i = 1 then 9 else 0 } } - match - execCompiledLetCallerLetStorageAddrReqEqLetMappingLetStorageSetMappingAddParamSetStorageAddParamStop - erc20Fields "ownerSlot" "balancesSlot" "totalSupplySlot" - "sender" "currentOwner" "currentBalance" "currentSupply" "to" "amount" - "Caller is not the owner" init, - execSourceLetCallerLetStorageAddrReqEqLetMappingLetStorageSetMappingAddParamSetStorageAddParamStop - init 0 2 1 "Caller is not the owner" with - | .revert c, .revert s => c = "Caller is not the owner" && c = s - | _, _ => false - -example : erc20MintNonOwnerAgreement = true := by - native_decide - --- ============================================================ --- ERC721 bridge theorem tests --- ============================================================ - -/-- Recognize the lowered owner-existence guard emitted from `not (owner == 0)`. -/ -def isOwnerExistsSwitch (ownerName : String) : Compiler.Yul.YulStmt → Bool - | .switch (.call "iszero" [.call "eq" [.ident ownerName', .lit 0]]) - [(1, [])] (some [.exprStmt (.call "revert" [.lit 0, .lit 0])]) => - ownerName' == ownerName - | _ => false - -/-- Recognize the lowered owner-match guard emitted from `addrToUint(ownerAddr) == ownerWord`. -/ -def isOwnerMatchSwitch (ownerAddrName ownerWordName : String) : Compiler.Yul.YulStmt → Bool - | .switch (.call "eq" [.ident ownerAddrName', .ident ownerWordName']) - [(1, [])] (some [.exprStmt (.call "revert" [.lit 0, .lit 0])]) => - ownerAddrName' == ownerAddrName && ownerWordName' == ownerWordName - | _ => false - -/-- ERC721.getApproved must keep the owner-existence guard and the corrected -slot layout from `Compiler.TypedIRCompilerCorrectness`. -/ -def compiledERC721GetApprovedHasExpectedBody : Bool := - match compileFunctionNamed Contracts.ERC721.spec "getApproved" with - | .error _ => false - | .ok block => - match block.params, lowerTStmts block.body with - | [tokenIdParam], - [ .let_ ownerWordName ownerRead - , ownerExistsGuard - , .let_ approvedWordName approvalRead - , .exprStmt (.call "mstore" [.lit 0, .ident approvedWordName']) - , .exprStmt (.call "return" [.lit 0, .lit 32]) - ] => - let tokenName := tVarName tokenIdParam - isMappingLoadExpr 4 tokenName ownerRead && - isOwnerExistsSwitch ownerWordName ownerExistsGuard && - isMappingLoadExpr 5 tokenName approvalRead && - approvedWordName' == approvedWordName - | _, _ => false - -/-- ERC721.getApproved emits the expected owner-guarded typed IR body. -/ -example : compiledERC721GetApprovedHasExpectedBody = true := by native_decide - -/-- ERC721.getApproved should revert for an unminted token. -/ -def compiledERC721GetApprovedRejectsUnminted : Bool := - match compileFunctionNamed Contracts.ERC721.spec "getApproved" with - | .error _ => false - | .ok block => - match block.params with - | [tokenIdParam] => - let initWorld : Verity.ContractState := - Verity.defaultState - let init : TExecState := - { world := initWorld - vars := { uint256 := fun i => - if i = tokenIdParam.id then 7 else 0 } } - match evalTBlock init block with - | .revert "Token does not exist" => true - | _ => false - | _ => false - -/-- ERC721.getApproved rejects unminted tokens. -/ -example : compiledERC721GetApprovedRejectsUnminted = true := by native_decide - -/-- ERC721.getApproved should load the approval from slot 5 after passing the -owner check on slot 4. -/ -def compiledERC721GetApprovedLoadsApproval : Bool := - match compileFunctionNamed Contracts.ERC721.spec "getApproved" with - | .error _ => false - | .ok block => - match block.params, returnedWordName? (lowerTStmts block.body) with - | [tokenIdParam], some returnName => - match tVarIdNamed? (block.params ++ block.locals) returnName with - | some returnVarId => - let initWorld : Verity.ContractState := - (Verity.defaultState.writeMapUint 4 7 999).writeMapUint 5 7 333 - let init : TExecState := - { world := initWorld - vars := { uint256 := fun i => - if i = tokenIdParam.id then 7 else 0 } } - match evalTBlock init block with - | .ok st => st.vars.uint256 returnVarId = 333 - | _ => false - | none => false - | _, _ => false - -/-- ERC721.getApproved returns the approval word from slot 5. -/ -example : compiledERC721GetApprovedLoadsApproval = true := by native_decide - -/-- ERC721.approve must keep both owner checks and write to slot 5. -/ -def compiledERC721ApproveHasExpectedBody : Bool := - match compileFunctionNamed Contracts.ERC721.spec "approve" with - | .error _ => false - | .ok block => - match block.params, lowerTStmts block.body with - | [approvedParam, tokenIdParam], - [ .let_ senderWordName (.call "caller" []) - , .let_ ownerWordName ownerRead - , ownerExistsGuard - , .let_ ownerAddrName (.ident senderWordName') - , ownerMatchGuard - , .exprStmt (.call "sstore" - [.call "mappingSlot" [.lit 5, .ident tokenName'], .ident approvedName']) - , .exprStmt (.call "stop" []) - ] => - let tokenName := tVarName tokenIdParam - let approvedName := tVarName approvedParam - isMappingLoadExpr 4 tokenName ownerRead && - isOwnerExistsSwitch ownerWordName ownerExistsGuard && - senderWordName' == senderWordName && - isOwnerMatchSwitch ownerAddrName ownerWordName ownerMatchGuard && - tokenName' == tokenName && - approvedName' == approvedName - | _, _ => false - -/-- ERC721.approve emits the expected guarded typed IR body. -/ -example : compiledERC721ApproveHasExpectedBody = true := by native_decide - -/-- ERC721.approve should reject unminted tokens. -/ -def compiledERC721ApproveRejectsUnminted : Bool := - match compileFunctionNamed Contracts.ERC721.spec "approve" with - | .error _ => false - | .ok block => - match block.params with - | [approvedParam, tokenIdParam] => - let initWorld : Verity.ContractState := - { Verity.defaultState with sender := 11 } - let init : TExecState := - { world := initWorld - vars := { address := fun i => if i = approvedParam.id then 33 else 0, - uint256 := fun i => if i = tokenIdParam.id then 7 else 0 } } - match evalTBlock init block with - | .revert "Token does not exist" => true - | _ => false - | _ => false - -/-- ERC721.approve rejects unminted tokens. -/ -example : compiledERC721ApproveRejectsUnminted = true := by native_decide - -/-- ERC721.approve should reject callers that are not the token owner. -/ -def compiledERC721ApproveRejectsNonOwner : Bool := - match compileFunctionNamed Contracts.ERC721.spec "approve" with - | .error _ => false - | .ok block => - match block.params with - | [approvedParam, tokenIdParam] => - let initWorld : Verity.ContractState := - { Verity.defaultState.writeMapUint 4 7 22 with sender := 11 } - let init : TExecState := - { world := initWorld - vars := { address := fun i => if i = approvedParam.id then 33 else 0, - uint256 := fun i => if i = tokenIdParam.id then 7 else 0 } } - match evalTBlock init block with - | .revert "Not token owner" => true - | _ => false - | _ => false - -/-- ERC721.approve enforces token-owner authorization. -/ -example : compiledERC721ApproveRejectsNonOwner = true := by native_decide - -/-- ERC721.approve writes approvals into slot 5 on success. -/ -def compiledERC721ApproveWritesApproval : Bool := - match compileFunctionNamed Contracts.ERC721.spec "approve" with - | .error _ => false - | .ok block => - match block.params with - | [approvedParam, tokenIdParam] => - let initWorld : Verity.ContractState := - { Verity.defaultState.writeMapUint 4 7 11 with sender := 11 } - let init : TExecState := - { world := initWorld - vars := { address := fun i => if i = approvedParam.id then 33 else 0, - uint256 := fun i => if i = tokenIdParam.id then 7 else 0 } } - match evalTBlock init block with - | .ok st => st.world.storageMapUint 5 7 = 33 - | _ => false - | _ => false - -/-- ERC721.approve writes the approval word to slot 5. -/ -example : compiledERC721ApproveWritesApproval = true := by native_decide - -open Compiler.CompilationModel in -example : findFieldSlot erc721Fields "ownersSlot" = some 4 := by - simpa using erc721OwnersSlotFieldSlot - -open Compiler.CompilationModel in -example : findFieldSlot erc721Fields "tokenApprovalsSlot" = some 5 := by - simpa using erc721TokenApprovalsSlotFieldSlot - -end Verity.Core.Free diff --git a/Contracts/VaultFromSolidity/Importer/Importer.lean b/Contracts/VaultFromSolidity/Importer/Importer.lean deleted file mode 100644 index a3bfd56460..0000000000 --- a/Contracts/VaultFromSolidity/Importer/Importer.lean +++ /dev/null @@ -1,1915 +0,0 @@ -import Lean -import Verity.Stdlib.Math -import Verity.Core.SolidityImportAttr -import Compiler.Sha256.Engine -import Contracts.VaultFromSolidity.Importer.Semantics - -/-! -A proof-only Solidity frontend. This module invokes pinned `solc --standard-json`, -validates a closed typed-AST/storage-layout subset, parses it into the closed, -intrinsically typed inductive in `Syntax.lean`, and registers each entry point as -`Semantics.lean`'s `Fn.meaning` applied to that parsed term. The accepted subset is -a kernel-checked Lean value, so nothing is serialized, no intermediate IR is -materialised, and no Lean source is generated or written to disk. - -Besides the executable model it elaborates a kernel-checked `Storage` structure -(the one use of the standard command elaborator in this frontend), registers -`view : ContractState → Storage` from the `Slot` handles, tags imported -declarations into the `solidity_import` simp set, and registers a deterministic -entry-point relation `step`. Specs can then say `v.totalAssets` instead of naming -a raw slot number. --/ - -open Lean Meta Elab Command - -namespace SolidityImporter - -/-- Release identity used in `sourceDigest`. Platform banners are not part of it. -/ -private def solcVersionPin := "0.8.33+commit.64118f21" -/-- Official SHA-256 digests from binaries.soliditylang.org `/list.json`. -/ -private def officialSolcSha256s : Array String := #[ - "1274e5c4621ae478090c5a1f48466fd3c5f658ed9e14b15a0b213dc806215468", - "8324280591ce398d7e2722846bc10ecf1779b13a328ef97b687c92cd9c70801a"] -private def acceptedSolcVersionOutputs : Array String := #[ - s!"solc, the solidity compiler commandline interface\nVersion: {solcVersionPin}.Linux.g++", - s!"solc, the solidity compiler commandline interface\nVersion: {solcVersionPin}.Darwin.appleclang"] -/-- Logical path × package-relative path. The digest covers this file, so adding -a source moves every imported `sourceDigest`. -/ -private def registeredSources : List (String × String) := [ - ("Contracts/VaultFromSolidity/Vault.sol", "Contracts/VaultFromSolidity/Vault.sol"), - ("Contracts/SolidityImportSmoke/Inheritance/Inheritance.sol", - "Contracts/SolidityImportSmoke/Inheritance/Inheritance.sol"), - ("Contracts/SolidityImportSmoke/Modifiers/Modifiers.sol", - "Contracts/SolidityImportSmoke/Modifiers/Modifiers.sol"), - ("Contracts/SolidityImportSmoke/Structs/Structs.sol", - "Contracts/SolidityImportSmoke/Structs/Structs.sol") -] - -private def field (j : Json) (key : String) : MetaM Json := - match j.getObjVal? key with - | .ok v => pure v - | .error e => throwError "{e}" - -private def field? (j : Json) (key : String) : Option Json := - (j.getObjVal? key).toOption - -private def str (j : Json) : MetaM String := - match j.getStr? with - | .ok v => pure v - | .error e => throwError "{e}" - -private def nat (j : Json) : MetaM Nat := - match j.getNat? with - | .ok v => pure v - | .error e => throwError "{e}" - -private def int (j : Json) : MetaM Int := - match j.getInt? with - | .ok v => pure v - | .error e => throwError "{e}" - -private def bool (j : Json) : MetaM Bool := - match j.getBool? with - | .ok v => pure v - | .error e => throwError "{e}" - -private def arr (j : Json) : MetaM (Array Json) := - match j.getArr? with - | .ok v => pure v - | .error e => throwError "{e}" - -private def objKeys (j : Json) : MetaM (List String) := - match j with - | .obj o => pure <| o.foldl (fun keys key _ => key :: keys) [] - | _ => throwError "object expected" - -private def requireKeys (j : Json) (allowed : List String) (what : String) : MetaM Unit := do - for key in ← objKeys j do - unless key ∈ allowed do throwError "unknown {what} field {key}" - -private def nodeKind (j : Json) : MetaM String := field j "nodeType" >>= str -private def nodeId (j : Json) : MetaM Nat := field j "id" >>= nat - -private partial def collectAstIds (j : Json) : MetaM (List Nat) := do - match j with - | .obj o => - let self ← if (field? j "nodeType").isSome then do pure [← nodeId j] else pure [] - let mut ids := self - for (_, value) in o.toList do ids := ids ++ (← collectAstIds value) - pure ids - | .arr xs => - let mut ids := [] - for value in xs do ids := ids ++ (← collectAstIds value) - pure ids - | _ => pure [] - -private def expect (ok : Bool) (message : String) : MetaM Unit := - unless ok do throwError message - -private def hexDigit (n : Nat) : Char := - if n < 10 then Char.ofNat ('0'.toNat + n) else Char.ofNat ('a'.toNat + n - 10) - -private def sha256Hex (bytes : ByteArray) : String := - (Sha256Engine.sha256 bytes).data.foldl (init := "") fun acc byte => - acc.push (hexDigit (byte.toNat / 16)) |>.push (hexDigit (byte.toNat % 16)) - -private def verifyCompiler (compiler : System.FilePath) : MetaM Unit := do - let output ← - if System.Platform.isOSX then - IO.Process.output { cmd := "/usr/bin/shasum", args := #["-a", "256", compiler.toString] } - else - IO.Process.output { cmd := "/usr/bin/sha256sum", args := #[compiler.toString] } - unless output.exitCode == 0 && officialSolcSha256s.contains (output.stdout.take 64).toString do - throwError "compiler checksum mismatch" - -private structure SourceContext where - path : System.FilePath - logicalPath : String - bytes : ByteArray - sourceId : Nat - -private def parseSpan (j : Json) : MetaM (Nat × Nat × Nat) := do - let pieces := (← str (← field j "src")).splitOn ":" - match pieces with - | [a, b, c] => - let some start := a.toNat? | throwError "invalid source span" - let some size := b.toNat? | throwError "invalid source span" - let some sourceId := c.toNat? | throwError "invalid source span" - pure (start, size, sourceId) - | _ => throwError "invalid source span" - -private def failAt (ctx : SourceContext) (j : Json) (why : String) : MetaM α := do - let (start, size, _sourceId) ← parseSpan j - let before := ctx.bytes.data.extract 0 (min start ctx.bytes.size) - let (line, column) := before.foldl - (fun (p : Nat × Nat) b => if b == 10 then (p.1 + 1, 1) else (p.1, p.2 + 1)) (1, 1) - let excerptBytes : ByteArray := ⟨ctx.bytes.data.extract start (min (start + size) (min ctx.bytes.size (start + 100)))⟩ - let excerpt := String.fromUTF8? excerptBytes |>.getD "" - let kind := (← nodeKind j) - throwError "{ctx.logicalPath}:{line}:{column}: {kind}: {why}\n{excerpt}" - -private def needAt (ctx : SourceContext) (j : Json) (ok : Bool) (why : String) : MetaM Unit := - unless ok do failAt ctx j why - -private def commonFields := ["id", "src", "nodeType"] -private def expressionFields := ["isConstant", "isLValue", "isPure", "lValueRequested", "typeDescriptions"] - -private def allowedNodeFields : String → Option (List String) - | "SourceUnit" => some ["absolutePath", "exportedSymbols", "license", "nodes"] - | "PragmaDirective" => some ["literals"] - | "ContractDefinition" => some ["abstract", "baseContracts", "canonicalName", "contractDependencies", - "contractKind", "documentation", "fullyImplemented", "linearizedBaseContracts", "name", "nameLocation", - "nodes", "scope", "usedErrors", "usedEvents", "storageLayout"] - | "StructuredDocumentation" => some ["text"] - | "VariableDeclaration" => some ["constant", "functionSelector", "mutability", "name", "nameLocation", "scope", - "stateVariable", "storageLocation", "typeDescriptions", "typeName", "visibility", "value"] - | "ElementaryTypeName" => some ["name", "stateMutability", "typeDescriptions"] - | "Mapping" => some ["keyName", "keyNameLocation", "keyType", "typeDescriptions", "valueName", - "valueNameLocation", "valueType"] - | "ErrorDefinition" => some ["errorSelector", "name", "nameLocation", "parameters"] - | "FunctionDefinition" => some ["body", "functionSelector", "implemented", "kind", "modifiers", "name", - "nameLocation", "parameters", "returnParameters", "scope", "stateMutability", "virtual", "visibility", - "documentation", "overrides", "baseFunctions"] - | "InheritanceSpecifier" => some ["baseName", "arguments"] - | "IdentifierPath" => some ["name", "nameLocations", "referencedDeclaration"] - | "OverrideSpecifier" => some ["overrides"] - | "ParameterList" => some ["parameters"] - | "Block" => some ["statements"] - | "ExpressionStatement" => some ["expression"] - | "Assignment" => some (expressionFields ++ ["leftHandSide", "operator", "rightHandSide"]) - | "BinaryOperation" => some (expressionFields ++ ["commonType", "leftExpression", "operator", "rightExpression", "function"]) - | "Identifier" => some ["argumentTypes", "name", "overloadedDeclarations", "referencedDeclaration", "typeDescriptions"] - | "MemberAccess" => some (expressionFields ++ ["expression", "memberLocation", "memberName", - "referencedDeclaration", "argumentTypes"]) - | "IndexAccess" => some (expressionFields ++ ["baseExpression", "indexExpression"]) - | "Literal" => some (expressionFields ++ ["hexValue", "kind", "subdenomination", "value"]) - | "FunctionCall" => some (expressionFields ++ ["arguments", "expression", "kind", "nameLocations", "names", "tryCall"]) - | "VariableDeclarationStatement" => some ["assignments", "declarations", "initialValue"] - | "IfStatement" => some ["condition", "trueBody", "falseBody"] - | "RevertStatement" => some ["errorCall"] - | "Return" => some ["expression", "functionReturnParameters"] - | "ModifierDefinition" => some ["body", "name", "nameLocation", "parameters", "virtual", "visibility", - "documentation"] - | "ModifierInvocation" => some ["arguments", "kind", "modifierName"] - | "PlaceholderStatement" => some ([] : List String) - | "StructDefinition" => some ["canonicalName", "members", "name", "nameLocation", "scope", "visibility"] - | "UserDefinedTypeName" => some ["pathNode", "referencedDeclaration", "typeDescriptions"] - | _ => none - -private def requiredNodeFields : String → List String - | "SourceUnit" => ["absolutePath", "exportedSymbols", "license", "nodes"] - | "PragmaDirective" => ["literals"] - | "ContractDefinition" => ["abstract", "baseContracts", "canonicalName", "contractDependencies", - "contractKind", "fullyImplemented", "linearizedBaseContracts", "name", "nameLocation", "nodes", - "scope", "usedErrors", "usedEvents"] - | "StructuredDocumentation" => ["text"] - | "VariableDeclaration" => ["constant", "mutability", "name", "nameLocation", "scope", "stateVariable", - "storageLocation", "typeDescriptions", "typeName", "visibility"] - | "ElementaryTypeName" => ["name", "typeDescriptions"] - | "Mapping" => ["keyName", "keyNameLocation", "keyType", "typeDescriptions", "valueName", - "valueNameLocation", "valueType"] - | "ErrorDefinition" => ["errorSelector", "name", "nameLocation", "parameters"] - | "FunctionDefinition" => ["implemented", "kind", "modifiers", "name", - "nameLocation", "parameters", "returnParameters", "scope", "stateMutability", "virtual", "visibility"] - | "InheritanceSpecifier" => ["baseName"] - | "IdentifierPath" => ["name", "nameLocations", "referencedDeclaration"] - | "OverrideSpecifier" => ["overrides"] - | "ParameterList" => ["parameters"] - | "Block" => ["statements"] - | "ExpressionStatement" => ["expression"] - | "Assignment" => expressionFields ++ ["leftHandSide", "operator", "rightHandSide"] - | "BinaryOperation" => expressionFields ++ ["commonType", "leftExpression", "operator", "rightExpression"] - | "Identifier" => ["name", "overloadedDeclarations", "referencedDeclaration", "typeDescriptions"] - | "MemberAccess" => expressionFields ++ ["expression", "memberLocation", "memberName", "typeDescriptions"] - | "IndexAccess" => expressionFields ++ ["baseExpression", "indexExpression", "typeDescriptions"] - | "Literal" => expressionFields ++ ["hexValue", "kind", "value", "typeDescriptions"] - | "FunctionCall" => expressionFields ++ ["arguments", "expression", "kind", "nameLocations", "names", - "tryCall", "typeDescriptions"] - | "VariableDeclarationStatement" => ["assignments", "declarations", "initialValue"] - | "IfStatement" => ["condition", "trueBody"] - | "RevertStatement" => ["errorCall"] - | "Return" => ["expression", "functionReturnParameters"] - | "ModifierDefinition" => ["name", "nameLocation", "parameters", "virtual", "visibility"] - | "ModifierInvocation" => ["kind", "modifierName"] - | "PlaceholderStatement" => [] - | "StructDefinition" => ["canonicalName", "members", "name", "nameLocation", "scope", "visibility"] - | "UserDefinedTypeName" => ["pathNode", "referencedDeclaration", "typeDescriptions"] - | _ => [] - -private def childFields : List String := ["nodes", "baseContracts", "parameters", "returnParameters", "body", - "statements", "typeName", "keyType", "valueType", "modifiers", "overrides", "storageLayout", "leftHandSide", - "rightHandSide", "leftExpression", "rightExpression", "expression", "baseExpression", "indexExpression", - "arguments", "declarations", "initialValue", "condition", "trueBody", "falseBody", "errorCall", "baseName", - "members", "modifierName", "pathNode"] - -private def validateTypeDescription (j : Json) : MetaM Unit := do - requireKeys j ["typeIdentifier", "typeString"] "type description" - let _ ← str (← field j "typeIdentifier") - let _ ← str (← field j "typeString") - -private def validateStringArray (j : Json) : MetaM Unit := do - for value in ← arr j do let _ ← str value - -private def validateNatArray (j : Json) : MetaM Unit := do - for value in ← arr j do let _ ← nat value - -private def validateAssignments (j : Json) : MetaM Unit := do - for value in ← arr j do unless value.isNull do let _ ← nat value - -private def validateExportedSymbols (j : Json) : MetaM Unit := do - match j with - | .obj entries => for (_, ids) in entries.toList do validateNatArray ids - | _ => throwError "expected exported-symbol object" - -private def validateMetadataField (ctx : SourceContext) (node : Json) (kind key : String) (value : Json) : MetaM Unit := do - if ["absolutePath", "canonicalName", "contractKind", "text", "mutability", "name", - "nameLocation", "scope", "storageLocation", "visibility", "stateMutability", "keyName", - "keyNameLocation", "valueName", "valueNameLocation", "errorSelector", "kind", "operator", - "memberLocation", "memberName", "hexValue", "value"].contains key then - if key == "scope" then let _ ← nat value - else let _ ← str value - else if ["abstract", "fullyImplemented", "constant", "stateVariable", "indexed", "implemented", - "virtual", "isConstant", "isLValue", "isPure", "lValueRequested", "tryCall"].contains key then - let _ ← bool value - else if key == "referencedDeclaration" then - unless value.isNull do let _ ← int value - else if key == "functionReturnParameters" then - let _ ← nat value - else if ["contractDependencies", "linearizedBaseContracts", "usedErrors", "usedEvents", - "baseFunctions", "overloadedDeclarations"].contains key then - validateNatArray value - else if ["literals", "nameLocations", "names"].contains key then - validateStringArray value - else if key == "assignments" then validateAssignments value - else if key == "exportedSymbols" then validateExportedSymbols value - else if ["license", "functionSelector", "subdenomination"].contains key then - unless value.isNull do let _ ← str value - else - failAt ctx node ("unvalidated AST metadata field " ++ kind ++ "." ++ key) - -private partial def validateNode (ctx : SourceContext) (j : Json) : MetaM Unit := do - let kind ← nodeKind j - let some allowed := allowedNodeFields kind | failAt ctx j "unsupported AST node" - let keys ← objKeys j - needAt ctx j (keys.all fun key => commonFields.contains key || allowed.contains key) - ("unexpected AST fields: " ++ String.intercalate ", " (keys.filter fun k => !(commonFields.contains k || allowed.contains k))) - let missing := (requiredNodeFields kind).filter fun key => !(keys.contains key) - needAt ctx j missing.isEmpty ("missing AST fields: " ++ String.intercalate ", " missing) - let _ ← nodeId j - let (start, size, sourceId) ← parseSpan j - needAt ctx j (sourceId == ctx.sourceId && start <= ctx.bytes.size && size <= ctx.bytes.size - start) - "source span outside registered source" - if let some description := field? j "typeDescriptions" then - validateTypeDescription description - if let some common := field? j "commonType" then - unless common.isNull do validateTypeDescription common - if let some arguments := field? j "argumentTypes" then - unless arguments.isNull do - for description in ← arr arguments do validateTypeDescription description - if let some documentation := field? j "documentation" then - if documentation.isNull then pure () - else match documentation with - | .str _ => pure () - | .obj _ => - needAt ctx j ((← nodeKind documentation) == "StructuredDocumentation") "invalid documentation" - validateNode ctx documentation - | _ => failAt ctx j "invalid documentation" - for key in keys do - unless commonFields.contains key || childFields.contains key || - ["typeDescriptions", "commonType", "argumentTypes", "documentation"].contains key || - (kind == "VariableDeclaration" && key == "value") do - try validateMetadataField ctx j kind key (← field j key) - catch _ => failAt ctx j ("invalid AST metadata field " ++ kind ++ "." ++ key) - if kind == "ContractDefinition" then - needAt ctx j ((field? j "storageLayout").all Json.isNull) "contract layout at specifier unsupported" - if kind == "VariableDeclaration" then - needAt ctx j ((field? j "value").all Json.isNull && (field? j "overrides").all Json.isNull) - "initializer/override unsupported" - if kind == "BinaryOperation" then - needAt ctx j ((field? j "function").all Json.isNull) "user-defined operator unsupported" - if kind == "FunctionCall" then - let k ← str (← field j "kind") - needAt ctx j ((k == "functionCall" || k == "structConstructorCall") && - !(← bool (← field j "tryCall"))) "unsupported call surface" - if k == "functionCall" then - needAt ctx j ((← arr (← field j "names")).isEmpty) "unsupported call surface" - for key in childFields do - if let some value := field? j key then - if key == "storageLayout" && kind == "ContractDefinition" then pure () - else if value.isNull then pure () - else match value with - | .arr values => for child in values do validateNode ctx child - | .obj _ => validateNode ctx value - | _ => failAt ctx j ("invalid AST child: " ++ key) - let requireKind (key : String) (kinds : List String) : MetaM Unit := do - let child ← field j key - needAt ctx j (kinds.contains (← nodeKind child)) ("unexpected child kind: " ++ key) - match kind with - | "SourceUnit" => - let _ ← arr (← field j "nodes") - | "ContractDefinition" => - let _ ← arr (← field j "nodes"); let _ ← arr (← field j "baseContracts") - | "InheritanceSpecifier" => - requireKind "baseName" ["IdentifierPath"] - if let some arguments := field? j "arguments" then - needAt ctx j ((← arr arguments).isEmpty) "constructor arguments unsupported" - | "OverrideSpecifier" => - let _ ← arr (← field j "overrides") - | "FunctionDefinition" => - match field? j "body" with - | none => - needAt ctx j (!(← bool (← field j "implemented")) && (← bool (← field j "virtual"))) - "unimplemented function must be virtual" - | some body => - if body.isNull then - needAt ctx j (!(← bool (← field j "implemented")) && (← bool (← field j "virtual"))) - "unimplemented function must be virtual" - else requireKind "body" ["Block"] - requireKind "parameters" ["ParameterList"] - requireKind "returnParameters" ["ParameterList"] - let _ ← arr (← field j "modifiers") - | "ParameterList" => - for p in (← arr (← field j "parameters")) do - needAt ctx j ((← nodeKind p) == "VariableDeclaration") "invalid parameter declaration" - | "Block" => let _ ← arr (← field j "statements") - | "VariableDeclaration" => requireKind "typeName" ["ElementaryTypeName", "Mapping", "UserDefinedTypeName"] - | "Mapping" => - let keyType ← field j "keyType" - if (← nodeKind keyType) == "UserDefinedTypeName" then - failAt ctx j "mapping with struct key" - requireKind "keyType" ["ElementaryTypeName"] - requireKind "valueType" ["ElementaryTypeName", "UserDefinedTypeName"] - | "ExpressionStatement" => let _ ← field j "expression" - | "Assignment" => let _ ← field j "leftHandSide"; let _ ← field j "rightHandSide" - | "BinaryOperation" => let _ ← field j "leftExpression"; let _ ← field j "rightExpression" - | "MemberAccess" => let _ ← field j "expression" - | "IndexAccess" => let _ ← field j "baseExpression"; let _ ← field j "indexExpression" - | "FunctionCall" => let _ ← field j "expression"; let _ ← arr (← field j "arguments") - | "VariableDeclarationStatement" => - for d in (← arr (← field j "declarations")) do - needAt ctx j ((← nodeKind d) == "VariableDeclaration") "invalid local declaration" - let _ ← field j "initialValue" - | "IfStatement" => - let trueBody ← field j "trueBody" - let tk ← nodeKind trueBody - needAt ctx j (tk == "Block" || tk == "RevertStatement") "unexpected child kind: trueBody" - | "RevertStatement" => requireKind "errorCall" ["FunctionCall"] - | "Return" => - if let some expr := field? j "expression" then - unless expr.isNull do let _ ← field j "expression" - | "ErrorDefinition" => requireKind "parameters" ["ParameterList"] - | "ModifierDefinition" => - requireKind "parameters" ["ParameterList"] - match field? j "body" with - | none => failAt ctx j "modifier missing body" - | some body => - if body.isNull then failAt ctx j "modifier missing body" - else requireKind "body" ["Block"] - | "ModifierInvocation" => - requireKind "modifierName" ["IdentifierPath"] - if let some arguments := field? j "arguments" then - unless arguments.isNull do let _ ← arr arguments - | "PlaceholderStatement" => pure () - | "StructDefinition" => - let members ← arr (← field j "members") - needAt ctx j (members.size == 2) "unsupported struct shape" - for m in members do - needAt ctx j ((← nodeKind m) == "VariableDeclaration") "invalid struct member" - let typ ← str (← field (← field m "typeDescriptions") "typeString") - unless typ == "uint256" || typ == "address" do - failAt ctx m "struct with a dynamic member" - | "UserDefinedTypeName" => requireKind "pathNode" ["IdentifierPath"] - | _ => pure () - -private structure StructMemberInfo where - name : String - ty : Sol.Ty - -private structure StructInfo where - id : Nat - name : String - canonicalName : String - members : List StructMemberInfo - -private structure FieldInfo where - id : Nat - var : String - name : String - getter : Option String - slot : Nat - sty : Sol.StorageTy - structId : Option Nat - memberIndex : Option Nat - -private structure OpaqueField where - id : Nat - label : String - slot : Nat - typeLabel : String - -private structure ModInfo where - id : Nat - name : String - contractId : Nat - node : Json - -private structure FnInfo where - id : Nat - contractId : Nat - contractName : String - name : String - visibility : String - mutability : String - virtual : Bool - implemented : Bool - isTarget : Bool - paramTys : List Sol.Ty - paramIds : List (Nat × Sol.Ty × String) - rets : List Sol.Ty - namedReturn : Option (Nat × Sol.Ty × String) - node : Json - -private def FnInfo.sig (f : FnInfo) : Sol.Sig := ⟨f.paramTys.reverse, f.rets⟩ - -private def FnInfo.leanIdent (f : FnInfo) : String := - if f.isTarget && (f.visibility == "public" || f.visibility == "external") then f.name - else f.contractName ++ "_" ++ f.name - -private def FnInfo.isEntry (f : FnInfo) : Bool := - f.implemented && (f.visibility == "public" || f.visibility == "external") - -/-- `view`/`pure` may appear in expression position. Nonpayable internals are -statement-only (`Stmt.callStmt`): pinned solc 0.8.x legacy codegen evaluates -an effectful call before the other operand / `+=` old-read. -/ -private def FnInfo.viewOrPure (f : FnInfo) : Bool := - f.mutability == "view" || f.mutability == "pure" - -private structure Frontend where - source : SourceContext - ast : Json - targetName : String - targetId : Nat - linearization : List Nat - contractById : List (Nat × String × Json) - fields : List FieldInfo - opaqueEntries : List OpaqueField - errors : List (Nat × String) - structs : List StructInfo - modifiers : List ModInfo - allFns : List FnInfo - functions : List FnInfo - digest : String - -private def validName (name : String) : Bool := - match name.toList with - | [] => false - | c :: cs => (c.isAlpha || c == '_') && (c.isAlpha || cs.any (·.isAlpha)) && - cs.all (fun c => c.isAlphanum || c == '_') && - !["sourceDigest", "Storage", "view", "step", "opaqueFields", "registeredSources"].contains name - -private def identifier (ctx : SourceContext) (j : Json) : MetaM String := do - let name ← str (← field j "name") - needAt ctx j (validName name) "unsupported/reserved name" - pure name - -private def typeString (j : Json) : MetaM String := - field j "typeDescriptions" >>= (field · "typeString") >>= str - -private def parseStructMembers (ctx : SourceContext) (node : Json) : MetaM (List StructMemberInfo) := do - let mut members : List StructMemberInfo := [] - for m in (← arr (← field node "members")) do - needAt ctx m (!(← bool (← field m "stateVariable")) && - !(← bool (← field m "constant")) && - (← str (← field m "mutability")) == "mutable" && - (← str (← field m "storageLocation")) == "default") "unsupported struct member" - let typ ← typeString m - let ty ← match typ with - | "uint256" => pure Sol.Ty.uint - | "address" => pure Sol.Ty.addr - | _ => failAt ctx m "struct with a dynamic member" - let name ← identifier ctx m - members := members ++ [StructMemberInfo.mk name ty] - needAt ctx node (!members.isEmpty) "empty struct unsupported" - needAt ctx node (members.map (·.ty) == [.uint, .addr]) "unsupported struct shape" - pure members - -private def lookupStruct (structs : List StructInfo) (id : Nat) : Option StructInfo := - structs.find? (·.id == id) - -private def parseValueTy (ctx : SourceContext) (structs : List StructInfo) (j : Json) : MetaM Sol.Ty := do - let typ ← typeString j - match typ with - | "uint256" => pure .uint - | "address" => pure .addr - | _ => - if typ.startsWith "struct " then - let some typeName := field? j "typeName" | failAt ctx j "unsupported value type" - needAt ctx j ((← nodeKind typeName) == "UserDefinedTypeName") "unsupported value type" - let rid ← int (← field typeName "referencedDeclaration") - needAt ctx j (rid >= 0) "unresolved struct" - let some info := lookupStruct structs rid.toNat | failAt ctx j "using an undeclared struct" - needAt ctx j (info.members.map (·.ty) == [.uint, .addr]) "unsupported struct shape" - pure (.pair info.id) - else failAt ctx j "unsupported value type" - -private def parseFnInfo (ctx : SourceContext) (structs : List StructInfo) - (contractId : Nat) (contractName : String) - (isTarget : Bool) (node : Json) : MetaM FnInfo := do - let name ← identifier ctx node - let mutability ← str (← field node "stateMutability") - needAt ctx node ((← str (← field node "kind")) == "function" && - ["internal", "public", "external", "private"].contains (← str (← field node "visibility")) && - ["nonpayable", "view", "pure"].contains mutability) - "unsupported function surface" - let implemented ← bool (← field node "implemented") - let virt ← bool (← field node "virtual") - if implemented then - needAt ctx node ((field? node "body").any fun b => !b.isNull) "implemented function missing body" - else - needAt ctx node virt "unimplemented function must be virtual" - let mut paramTys : List Sol.Ty := [] - let mut paramIds : List (Nat × Sol.Ty × String) := [] - for p in (← arr (← field (← field node "parameters") "parameters")) do - let loc ← str (← field p "storageLocation") - needAt ctx node (!(← bool (← field p "stateVariable")) && - !(← bool (← field p "constant")) && - (← str (← field p "mutability")) == "mutable" && - (loc == "default" || loc == "memory") && - (← str (← field p "visibility")) == "internal" && - (field? p "value").all Json.isNull) "unsupported parameter declaration" - let ty ← parseValueTy ctx structs p - match ty with - | .pair _ => needAt ctx p (loc == "memory") "unsupported parameter declaration" - | _ => needAt ctx p (loc == "default") "unsupported parameter declaration" - let pname ← identifier ctx p - paramTys := paramTys ++ [ty] - paramIds := paramIds ++ [(← nodeId p, ty, pname)] - let rs ← arr (← field (← field node "returnParameters") "parameters") - needAt ctx node (rs.size <= 1) "unsupported signature" - let mut rets : List Sol.Ty := [] - let mut namedReturn : Option (Nat × Sol.Ty × String) := none - for r in rs do - let loc ← str (← field r "storageLocation") - needAt ctx node (!(← bool (← field r "stateVariable")) && - !(← bool (← field r "constant")) && - (← str (← field r "mutability")) == "mutable" && - (loc == "default" || loc == "memory") && - (← str (← field r "visibility")) == "internal" && - (field? r "value").all Json.isNull) "unsupported parameter declaration" - let ty ← parseValueTy ctx structs r - match ty with - | .pair _ => needAt ctx r (loc == "memory") "unsupported return type" - | .uint => needAt ctx r (loc == "default") "unsupported return type" - | .addr => needAt ctx r (loc == "default") "unsupported return type" - rets := [ty] - let rname ← str (← field r "name") - if !rname.isEmpty then - needAt ctx node (validName rname) "unsupported/reserved name" - namedReturn := some (← nodeId r, ty, rname) - pure { - id := ← nodeId node, contractId, contractName, name, - visibility := ← str (← field node "visibility"), - mutability, virtual := virt, implemented, isTarget, paramTys, paramIds, rets, namedReturn, node - } - -set_option maxRecDepth 2048 in -private def parseCompilerOutput (sourcePath : System.FilePath) (logicalPath : String) - (raw : ByteArray) (outputText importerText : String) (targetName? : Option String) : - MetaM Frontend := do - let output ← match Json.parse outputText with - | .ok j => pure j - | .error e => throwError "invalid solc standard JSON: {e}" - requireKeys output ["contracts", "sources", "errors"] "solc output" - if let some errors := field? output "errors" then - for e in (← arr errors) do - -- Compiler diagnostics may grow extra locator fields; only the fields we - -- read are required. Unknown *AST* fields still fail closed. - let keys ← objKeys e - for need in ["formattedMessage", "message", "severity"] do - unless keys.contains need do throwError "missing compiler diagnostic field {need}" - if (← str (← field e "severity")) == "error" then - throwError "{← str (← field e "formattedMessage")}" - let sources ← field output "sources" - let sourceKeys ← objKeys sources - expect (sourceKeys == [logicalPath]) "unexpected compiler sources" - let sourceOut ← field sources logicalPath - requireKeys sourceOut ["ast", "id"] "source output" - let sourceId ← nat (← field sourceOut "id") - let ast ← field sourceOut "ast" - let ctx := { path := sourcePath, logicalPath, bytes := raw, sourceId } - needAt ctx ast ((← nodeKind ast) == "SourceUnit") "root must be SourceUnit" - validateNode ctx ast - needAt ctx ast ((← str (← field ast "absolutePath")) == logicalPath) - "source-unit path mismatch" - let ids ← collectAstIds ast - needAt ctx ast (ids.length == ids.eraseDups.length) "duplicate Solidity AST node ID" - let mut contracts : List Json := [] - for node in (← arr (← field ast "nodes")) do - match ← nodeKind node with - | "PragmaDirective" => - let literals ← arr (← field node "literals") - needAt ctx node (literals.size == 4 && (← str literals[0]!) == "solidity" && - (← str literals[1]!) == "^" && (← str literals[2]!) == "0.8" && - (← str literals[3]!) == ".33") "unsupported pragma" - | "ContractDefinition" => contracts := contracts ++ [node] - | "StructDefinition" => pure () - | "ErrorDefinition" => pure () - | _ => failAt ctx node "unsupported source declaration" - needAt ctx ast (!contracts.isEmpty) "exactly one concrete contract required" - let mut concrete : List Json := [] - for c in contracts do - unless (← bool (← field c "abstract")) do concrete := concrete ++ [c] - let target ← match targetName? with - | none => - needAt ctx ast (concrete.length == 1) "exactly one concrete contract required" - pure concrete.head! - | some name => - let some c := contracts.find? fun n => (field? n "name").bind (·.getStr?.toOption) == some name - | throwError "unknown contract {name}" - needAt ctx c (!(← bool (← field c "abstract")) && (← bool (← field c "fullyImplemented"))) - "target contract must be fully implemented" - pure c - let targetId ← nodeId target - let targetName ← str (← field target "name") - needAt ctx target ((← str (← field target "contractKind")) == "contract" && - !(← bool (← field target "abstract")) && (← bool (← field target "fullyImplemented")) && - (← arr (← field target "usedEvents")).isEmpty) - "inheritance/abstract contract unsupported" - let mut contractById : List (Nat × String × Json) := [] - let mut contractIds : List Nat := [] - for c in contracts do - needAt ctx c ((← str (← field c "contractKind")) == "contract") "unsupported contract kind" - needAt ctx c ((← str (← field c "canonicalName")) == (← str (← field c "name")) && - (← nat (← field c "scope")) == (← nodeId ast)) "contract identity mismatch" - let cid ← nodeId c - contractById := contractById ++ [(cid, ← str (← field c "name"), c)] - contractIds := contractIds ++ [cid] - for c in contracts do - for dep in (← arr (← field c "contractDependencies")) do - needAt ctx c (contractIds.contains (← nat dep)) "is referencing a contract from another file" - for b in (← arr (← field c "baseContracts")) do - let baseName ← field b "baseName" - let rid ← int (← field baseName "referencedDeclaration") - needAt ctx b (rid >= 0 && contractIds.contains rid.toNat) - "is referencing a contract from another file" - let linearized ← arr (← field target "linearizedBaseContracts") - needAt ctx target (linearized.size >= 1 && (← nat linearized[0]!) == targetId) - "invalid contract linearization" - let mut linearization : List Nat := [] - for idj in linearized do - let id ← nat idj - needAt ctx target (contractIds.contains id) "invalid contract linearization" - linearization := linearization ++ [id] - let exports ← field ast "exportedSymbols" - needAt ctx ast ((← objKeys exports).contains targetName) "exported symbol mismatch" - let exportedIds ← arr (← field exports targetName) - needAt ctx ast (exportedIds.any fun id => (id.getNat?.toOption == some targetId)) - "exported symbol mismatch" - let compilerContracts ← field output "contracts" - expect ((← objKeys compilerContracts) == [logicalPath]) "unexpected compiler contract sources" - let sourceContracts ← field compilerContracts logicalPath - expect ((← objKeys sourceContracts).contains targetName) "unexpected compiler contracts" - let contractOut ← field sourceContracts targetName - requireKeys contractOut ["storageLayout"] "contract output" - let layout ← field contractOut "storageLayout" - requireKeys layout ["storage", "types"] "storage layout" - let storage ← arr (← field layout "storage") - let layoutTypes ← field layout "types" - let mut fields : List FieldInfo := [] - let mut opaqueEntries : List OpaqueField := [] - let mut errors : List (Nat × String) := [] - let mut structs : List StructInfo := [] - let mut modifiers : List ModInfo := [] - let mut allFns : List FnInfo := [] - for node in (← arr (← field ast "nodes")) do - if (← nodeKind node) == "StructDefinition" then - let members ← parseStructMembers ctx node - structs := structs ++ [{ - id := ← nodeId node - name := ← identifier ctx node - canonicalName := ← str (← field node "canonicalName") - members }] - else if (← nodeKind node) == "ErrorDefinition" then - let ps ← arr (← field (← field node "parameters") "parameters") - needAt ctx node ps.isEmpty "only zero-argument custom errors" - errors := errors ++ [((← nodeId node), (← identifier ctx node))] - for cid in linearization do - let some (_, _, cnode) := contractById.find? (·.1 == cid) - | failAt ctx target "invalid contract linearization" - for node in (← arr (← field cnode "nodes")) do - if (← nodeKind node) == "StructDefinition" then - let members ← parseStructMembers ctx node - structs := structs ++ [{ - id := ← nodeId node - name := ← identifier ctx node - canonicalName := ← str (← field node "canonicalName") - members }] - for cid in linearization do - let some (_, cname, cnode) := contractById.find? (·.1 == cid) - | failAt ctx target "invalid contract linearization" - for node in (← arr (← field cnode "nodes")) do - match ← nodeKind node with - | "VariableDeclaration" => - needAt ctx node ((← bool (← field node "stateVariable")) && - !(← bool (← field node "constant")) && - (← str (← field node "mutability")) == "mutable" && - (field? node "value").all Json.isNull && - (← str (← field node "storageLocation")) == "default" && - (← nat (← field node "scope")) == cid) "initializer/constant/transient field unsupported" - let id ← nodeId node - let some entry := storage.find? fun e => (field? e "astId").bind (·.getNat?.toOption) == some id - | failAt ctx node "missing storage layout" - requireKeys entry ["astId", "contract", "label", "offset", "slot", "type"] "storage entry" - let label ← str (← field entry "label") - needAt ctx node ((← str (← field entry "contract")) == s!"{logicalPath}:{targetName}" && - (← str (← field node "name")) == label) "storage declaration mismatch" - needAt ctx node ((← nat (← field entry "offset")) == 0) "missing/packed layout" - let typ ← typeString node - let typeId ← str (← field entry "type") - let layoutType ← field layoutTypes typeId - let slotText ← str (← field entry "slot") - let some slot := slotText.toNat? | failAt ctx node "invalid storage slot" - let name ← identifier ctx node - if typ == "uint256" || typ == "mapping(address => uint256)" || typ == "address" then - let sty : Sol.StorageTy := - if typ == "mapping(address => uint256)" then .mapping - else if typ == "address" then .addr else .scalar - let expectedTypeKeys := - if sty == .mapping then ["encoding", "key", "label", "numberOfBytes", "value"] - else ["encoding", "label", "numberOfBytes"] - requireKeys layoutType expectedTypeKeys "storage type" - match sty with - | .scalar => - needAt ctx node ((← str (← field layoutType "numberOfBytes")) == "32" && - (← str (← field layoutType "encoding")) == "inplace" && - (← str (← field layoutType "label")) == "uint256") "bad scalar layout" - | .addr => - needAt ctx node ((← str (← field layoutType "numberOfBytes")) == "20" && - (← str (← field layoutType "encoding")) == "inplace" && - (← str (← field layoutType "label")) == "address") "bad scalar layout" - | .mapping => - needAt ctx node ((← str (← field layoutType "numberOfBytes")) == "32") "nonword layout" - let keyType ← field layoutTypes (← str (← field layoutType "key")) - let valueType ← field layoutTypes (← str (← field layoutType "value")) - requireKeys keyType ["encoding", "label", "numberOfBytes"] "mapping key type" - requireKeys valueType ["encoding", "label", "numberOfBytes"] "mapping value type" - needAt ctx node ((← str (← field layoutType "encoding")) == "mapping" && - (← str (← field keyType "encoding")) == "inplace" && - (← str (← field keyType "label")) == "address" && - (← str (← field keyType "numberOfBytes")) == "20" && - (← str (← field valueType "encoding")) == "inplace" && - (← str (← field valueType "label")) == "uint256" && - (← str (← field valueType "numberOfBytes")) == "32") "bad mapping layout" - let vis ← str (← field node "visibility") - let getter := if vis == "public" then some name else none - fields := fields ++ [FieldInfo.mk id name (name ++ "Slot") getter slot sty none none] - else if typ.startsWith "struct " then - let some typeName := field? node "typeName" | failAt ctx node "unsupported value type" - needAt ctx node ((← nodeKind typeName) == "UserDefinedTypeName") "unsupported value type" - let rid ← int (← field typeName "referencedDeclaration") - needAt ctx node (rid >= 0) "unresolved struct" - let some info := lookupStruct structs rid.toNat | failAt ctx node "using an undeclared struct" - requireKeys layoutType ["encoding", "label", "members", "numberOfBytes"] "storage type" - needAt ctx node ((← str (← field layoutType "encoding")) == "inplace") "bad scalar layout" - let vis ← str (← field node "visibility") - let mut i : Nat := 0 - for mem in info.members do - let sty : Sol.StorageTy := match mem.ty with - | .uint => .scalar - | .addr => .addr - | .pair _ => .scalar - let mname := name ++ "_" ++ mem.name - let getter := if vis == "public" && i == 0 then some name else none - fields := fields ++ [FieldInfo.mk id mname (mname ++ "Slot") getter (slot + i) sty (some info.id) (some i)] - i := i + 1 - else - let vis ← str (← field node "visibility") - needAt ctx node (vis != "public") "opaque public getter unsupported" - let typeLabel ← str (← field layoutType "label") - opaqueEntries := opaqueEntries ++ [OpaqueField.mk id label slot typeLabel] - | "ErrorDefinition" => - let ps ← arr (← field (← field node "parameters") "parameters") - needAt ctx node ps.isEmpty "only zero-argument custom errors" - errors := errors ++ [((← nodeId node), (← identifier ctx node))] - | "FunctionDefinition" => - needAt ctx node ((← nat (← field node "scope")) == cid) "function scope mismatch" - allFns := allFns ++ [← parseFnInfo ctx structs cid cname (cid == targetId) node] - | "ModifierDefinition" => - needAt ctx node (!(← bool (← field node "virtual"))) "unsupported modifier" - let ps ← arr (← field (← field node "parameters") "parameters") - needAt ctx node ps.isEmpty "modifier arguments" - let mname ← identifier ctx node - if modifiers.any (·.name == mname) then failAt ctx node "unsupported/reserved name" - modifiers := modifiers ++ [{ id := ← nodeId node, name := mname, contractId := cid, node }] - | "StructDefinition" => pure () - | _ => failAt ctx node "unsupported contract declaration" - let accounted := storage.all fun e => - (field? e "astId").bind (·.getNat?.toOption) |>.any fun id => - fields.any (·.id == id) || opaqueEntries.any (·.id == id) - needAt ctx target accounted "unaccounted layout field" - let names := fields.flatMap fun f => f.name :: f.getter.toList - needAt ctx target (names.length == names.eraseDups.length) "storage/generated name collision" - let slots := (fields.map (·.slot)) ++ (opaqueEntries.map (·.slot)) - needAt ctx target (slots.length == slots.eraseDups.length) "storage slot collision" - let mut usedErrorIds : List Nat := [] - for cid in linearization do - let some (_, _, cnode) := contractById.find? (·.1 == cid) | failAt ctx target "invalid contract linearization" - for errorId in (← arr (← field cnode "usedErrors")) do - usedErrorIds := usedErrorIds ++ [← nat errorId] - needAt ctx target (usedErrorIds.all fun id => errors.any (·.1 == id)) "custom error reference mismatch" - for f in allFns do - for g in allFns do - needAt ctx f.node (!(f.name == g.name && f.paramTys != g.paramTys)) - "function overloading by parameter type unsupported" - let functions := allFns.filter (·.implemented) - let sourceText := String.fromUTF8? raw |>.getD "" - let digest := sha256Hex (sourceText ++ outputText ++ importerText ++ solcVersionPin).toUTF8 - pure { - source := ctx, ast, targetName, targetId, linearization, contractById, - fields, opaqueEntries, errors, structs, modifiers, allFns, functions, digest - } - -private def uint := mkConst ``Verity.Core.Uint256 -private def address := mkConst ``Verity.Core.Address - -private def register (name : Name) (value : Expr) (type? : Option Expr := none) - (simp : Bool := true) : MetaM Unit := do - if (← getEnv).contains name then throwError "declaration collision: {name}" - let value ← instantiateMVars value - let type ← instantiateMVars (← type?.getDM (inferType value)) - if value.hasMVar || value.hasFVar || type.hasMVar || type.hasFVar then - throwError "unclosed imported declaration {name}" - addDecl (.defnDecl { name, levelParams := [], type, value, hints := .regular 0, safety := .safe }) - (forceExpose := true) - compileDecls #[name] (logErrors := false) - if simp then - let some ext ← getSimpExtension? `solidity_import - | throwError "solidity_import simp set is not registered" - ext.add (SimpEntry.toUnfold name) AttributeKind.global - -private def requireType (frontend : Frontend) (j : Json) (expected : String) : MetaM Unit := do - let actual ← typeString j - if expected.startsWith "struct " then - needAt frontend.source j (actual == expected || actual == expected ++ " memory" || - actual == expected ++ " storage ref") ("expected " ++ expected) - else - let identifier ← str (← field (← field j "typeDescriptions") "typeIdentifier") - let expectedIdentifier := match expected with - | "uint256" => "t_uint256" - | "address" => "t_address" - | "mapping(address => uint256)" => "t_mapping$_t_address_$_t_uint256_$" - | "msg" => "t_magic_message" - | "bool" => "t_bool" - | "tuple()" => "t_tuple$__$" - | _ => "" - needAt frontend.source j (actual == expected && identifier == expectedIdentifier) ("expected " ++ expected) - -private def requireUintish (frontend : Frontend) (j : Json) : MetaM Unit := do - let actual ← typeString j - if actual.startsWith "int_const " then pure () - else requireType frontend j "uint256" - -private def valueType : Sol.Ty → Expr - | .uint => uint - | .addr => address - | .pair _ => mkConst ``Sol.Pair - -private def typeName : Sol.Ty → String - | .uint => "uint256" - | .addr => "address" - | .pair _ => "struct" - -private def pairTypeNameOf (info : StructInfo) : String := - "struct " ++ info.canonicalName - -private def pairTypeName (frontend : Frontend) (id : Nat) : Option String := - (lookupStruct frontend.structs id).map pairTypeNameOf - -private def isPairTypeString (frontend : Frontend) (typ : String) : Bool := - frontend.structs.any (fun s => - s.members.map (·.ty) == [.uint, .addr] && - (typ == pairTypeNameOf s || typ == pairTypeNameOf s ++ " memory" || - typ == pairTypeNameOf s ++ " storage ref")) - -private def lookupStructByTypeString (frontend : Frontend) (typ : String) : Option StructInfo := - frontend.structs.find? (fun s => - s.members.map (·.ty) == [.uint, .addr] && - (typ == pairTypeNameOf s || typ == pairTypeNameOf s ++ " memory" || - typ == pairTypeNameOf s ++ " storage ref")) - -private def resultType : List Sol.Ty → Expr - | [] => mkConst ``Unit - | [t] => valueType t - | _ => mkConst ``Unit - -private def layoutOf (fields : List FieldInfo) : Sol.Layout := fields.map (·.sty) - -private def fieldIndex (fields : List FieldInfo) (id : Nat) : Option Nat := - go fields 0 -where - go : List FieldInfo → Nat → Option Nat - | [], _ => none - | f :: fs, i => if f.id == id then some i else go fs (i + 1) - -private def fieldIndexAt (fields : List FieldInfo) (pos : Nat) : Option Nat := - if pos < fields.length then some pos else none - -private def svarAt (frontend : Frontend) (L : Sol.Layout) (pos : Nat) (s : Sol.StorageTy) - (j : Json) : MetaM (Sol.SVar L s) := do - let some entry := Sol.SVar.ofIndex L pos | failAt frontend.source j "unresolved declaration id" - if h : entry.1 = s then pure (h ▸ entry.2) else failAt frontend.source j "storage type mismatch" - -private def svar (frontend : Frontend) (L : Sol.Layout) (id : Nat) (s : Sol.StorageTy) - (j : Json) : MetaM (Sol.SVar L s) := do - let some i := fieldIndex frontend.fields id | failAt frontend.source j "unresolved declaration id" - svarAt frontend L i s j - -private structure Scope (Γ : Sol.Ctx) where - vars : List (Nat × Σ t, Sol.Var Γ t) - -private def Scope.push (sc : Scope Γ) (id : Nat) (t : Sol.Ty) : Scope (t :: Γ) := - ⟨(id, ⟨t, .here⟩) :: sc.vars.map fun v => (v.1, ⟨v.2.1, .there v.2.2⟩)⟩ - -private def Scope.find? (sc : Scope Γ) (id : Nat) : Option (Σ t, Sol.Var Γ t) := - (sc.vars.find? fun v => v.1 == id).map (·.2) - -private def varHandles : (Γ : Sol.Ctx) → List (Σ t, Sol.Var Γ t) - | [] => [] - | t :: ts => ⟨t, .here⟩ :: (varHandles ts).map fun v => ⟨v.1, .there v.2⟩ - -private def scopeOf (params : List (Nat × Sol.Ty × String)) (Γ : Sol.Ctx) : Scope Γ := - ⟨(varHandles Γ).zip params.reverse |>.map fun entry => (entry.2.1, entry.1)⟩ - -private def slotsTerm : (handles : List (Sol.StorageTy × Expr)) → MetaM Expr - | [] => pure (mkConst ``Sol.Slots.nil) - | (st, h) :: rest => do - let restE ← slotsTerm rest - let tail : Sol.Layout := rest.map (·.1) - mkAppOptM ``Sol.Slots.cons #[some (toExpr tail), some (toExpr st), some h, some restE] - -private def envTerm : (xs : List (Expr × Sol.Ty)) → MetaM Expr - | [] => pure (mkConst ``Sol.Env.nil) - | (x, t) :: rest => do - let restE ← envTerm rest - let tail : Sol.Ctx := rest.map (·.2) - mkAppOptM ``Sol.Env.cons #[some (toExpr tail), some (toExpr t), some x, some restE] - -private partial def withParams (ps : List (Nat × Sol.Ty × String)) (acc : List (Expr × Sol.Ty)) - (k : List (Expr × Sol.Ty) → MetaM Expr) : MetaM Expr := do - match ps with - | [] => k acc - | p :: rest => - withLocalDeclD (Name.mkSimple p.2.2) (valueType p.2.1) fun x => do - mkLambdaFVars #[x] (← withParams rest ((x, p.2.1) :: acc) k) - -private def familyOf (frontend : Frontend) (name : String) (paramTys : List Sol.Ty) : List FnInfo := - frontend.allFns.filter fun f => f.name == name && f.paramTys == paramTys - -private def mostDerived (frontend : Frontend) (name : String) (paramTys : List Sol.Ty) : Option FnInfo := - Id.run do - for cid in frontend.linearization do - if let some f := (familyOf frontend name paramTys).find? (fun g => g.contractId == cid && g.implemented) then - return some f - none - -private def superTarget (frontend : Frontend) (definingId : Nat) (name : String) - (paramTys : List Sol.Ty) : Option FnInfo := - Id.run do - let mut seen := false - for cid in frontend.linearization do - if cid == definingId then seen := true - else if seen then - if let some f := (familyOf frontend name paramTys).find? (fun g => g.contractId == cid && g.implemented) then - return some f - none - -private def lookupFn (frontend : Frontend) (id : Nat) : Option FnInfo := - frontend.allFns.find? (·.id == id) - -private def isErrorRef (frontend : Frontend) (id : Nat) : Bool := - frontend.errors.any (·.1 == id) - -private def resolveCall (frontend : Frontend) (_current : FnInfo) (definingId : Nat) - (call : Json) : MetaM FnInfo := do - let callee ← field call "expression" - match ← nodeKind callee with - | "Identifier" => - let rid ← int (← field callee "referencedDeclaration") - needAt frontend.source callee (rid >= 0) "unresolved builtin identifier" - let id := rid.toNat - if isErrorRef frontend id then failAt frontend.source call "unsupported revert" - let some static := lookupFn frontend id | failAt frontend.source call "unresolved function reference" - needAt frontend.source call ((familyOf frontend static.name static.paramTys).any (·.id == id)) - "virtual dispatch disagrees with AST" - if static.visibility == "private" || !static.virtual then - pure static - else - let some resolved := mostDerived frontend static.name static.paramTys - | failAt frontend.source call "unimplemented virtual" - pure resolved - | "MemberAccess" => - let base ← field callee "expression" - needAt frontend.source callee ((← nodeKind base) == "Identifier" && - (← str (← field base "name")) == "super" && - (← int (← field base "referencedDeclaration")) == -25) - "only super.f() member calls supported" - let rid ← int (← field callee "referencedDeclaration") - needAt frontend.source callee (rid >= 0) "unresolved super target" - let some static := lookupFn frontend rid.toNat - | failAt frontend.source call "unresolved function reference" - -- solc's AST `referencedDeclaration` on `super.f` is the next override in - -- the *defining* contract's linearization. Runtime (and this importer) - -- use the *target* contract's C3 (`linearizedBaseContracts`), which - -- disagrees on diamonds. Follow `superTarget`; only require the AST id - -- to name a member of the same virtual family. - needAt frontend.source callee - ((familyOf frontend static.name static.paramTys).any (·.id == rid.toNat)) - "super dispatch family mismatch" - let some resolved := superTarget frontend definingId static.name static.paramTys - | failAt frontend.source call "unresolved super target" - pure resolved - | _ => failAt frontend.source call "unsupported call surface" - -private partial def collectCallIds (frontend : Frontend) (current : FnInfo) (definingId : Nat) - (j : Json) : MetaM (List Nat) := do - match j with - | .obj o => - let walkChildren : MetaM (List Nat) := do - let rest ← o.toList.mapM fun (_, v) => collectCallIds frontend current definingId v - pure rest.flatten - match field? j "nodeType" with - | some nt => - if (← str nt) == "FunctionCall" then - if (field? j "kind").bind (·.getStr?.toOption) == some "structConstructorCall" then - walkChildren - else do - let callee ← field j "expression" - let skip ← match ← nodeKind callee with - | "Identifier" => - let rid ← int (← field callee "referencedDeclaration") - pure (rid >= 0 && isErrorRef frontend rid.toNat) - | _ => pure false - let restIds ← walkChildren - if skip then pure restIds - else - let resolved ← resolveCall frontend current definingId j - pure (resolved.id :: restIds) - else walkChildren - | none => walkChildren - | .arr xs => - let rest ← xs.toList.mapM (collectCallIds frontend current definingId) - pure rest.flatten - | _ => pure [] - -private def lookupMod (frontend : Frontend) (id : Nat) : Option ModInfo := - frontend.modifiers.find? (·.id == id) - -private partial def countPlaceholders (j : Json) : Nat := - match j with - | .obj o => - let here := if (j.getObjVal? "nodeType").toOption == some (Json.str "PlaceholderStatement") then 1 else 0 - o.foldl (fun acc _ v => acc + countPlaceholders v) here - | .arr xs => xs.foldl (fun acc v => acc + countPlaceholders v) 0 - | _ => 0 - -private def splitModifierBody (stmts : List Json) : MetaM (List Json × List Json) := do - let mut pre : List Json := [] - let mut post : List Json := [] - let mut seen := false - for s in stmts do - if (← nodeKind s) == "PlaceholderStatement" then - if seen then throwError "modifier with two placeholders" - seen := true - else if seen then post := post ++ [s] - else pre := pre ++ [s] - unless seen do throwError "modifier missing placeholder" - pure (pre, post) - -private def fnModifierInvocations (fn : FnInfo) : MetaM (Array Json) := do - arr (← field fn.node "modifiers") - -private def topoSort (frontend : Frontend) : MetaM (List FnInfo) := do - let mut deps : List (Nat × List Nat) := [] - for f in frontend.functions do - let body := field? f.node "body" |>.getD Json.null - let mut callees := (← collectCallIds frontend f f.contractId body).eraseDups - for inv in (← fnModifierInvocations f) do - let nameNode ← field inv "modifierName" - let rid ← int (← field nameNode "referencedDeclaration") - needAt frontend.source inv (rid >= 0) "unresolved modifier" - let some m := lookupMod frontend rid.toNat | failAt frontend.source inv "unresolved modifier" - let mbody := field? m.node "body" |>.getD Json.null - callees := (callees ++ (← collectCallIds frontend f m.contractId mbody)).eraseDups - deps := deps ++ [(f.id, callees)] - let mut remaining := frontend.functions - let mut order : List FnInfo := [] - for _ in List.range (frontend.functions.length + 1) do - if remaining.isEmpty then return order - match remaining.find? fun f => - ((deps.find? (·.1 == f.id)).map (·.2) |>.getD []).all fun c => - order.any (·.id == c) with - | some next => - order := order ++ [next] - remaining := remaining.filter (fun g => g.id != next.id) - | none => - failAt frontend.source frontend.ast - s!"direct and mutual recursion unsupported ({String.intercalate ", " (remaining.map (·.leanIdent))})" - failAt frontend.source frontend.ast "direct and mutual recursion unsupported" - -private structure ParseCtx where - frontend : Frontend - L : Sol.Layout - F : Sol.Fns - registered : List FnInfo - current : FnInfo - superFrom : Nat - -private def indexOfFn : List FnInfo → Nat → Nat → Option Nat - | [], _, _ => none - | x :: xs, id, i => if x.id == id then some i else indexOfFn xs id (i + 1) - -private def fvarOf (ctx : ParseCtx) (resolved : FnInfo) (j : Json) : MetaM (Σ σ, Sol.FVar ctx.F σ) := do - let some idx := indexOfFn ctx.registered resolved.id 0 - | failAt ctx.frontend.source j "direct and mutual recursion unsupported" - let some entry := Sol.FVar.ofIndex ctx.F idx - | failAt ctx.frontend.source j "unresolved function reference" - unless entry.1 == resolved.sig do failAt ctx.frontend.source j "function signature mismatch" - pure entry - -mutual -private partial def loadPair (ctx : ParseCtx) {Γ : Sol.Ctx} (id : Nat) (sid : Nat) (j : Json) : - MetaM (Sol.Expr ctx.L ctx.F Γ (.pair sid)) := do - let members := (List.range ctx.frontend.fields.length).zip ctx.frontend.fields |>.filter (fun p => p.2.id == id) - match members with - | (p0, m0) :: (p1, m1) :: [] => - needAt ctx.frontend.source j (m0.sty == .scalar && m1.sty == .addr) "storage type mismatch" - let a ← svarAt ctx.frontend ctx.L p0 .scalar j - let b ← svarAt ctx.frontend ctx.L p1 .addr j - pure (.pair (.load a) (.loadAddr b)) - | _ => failAt ctx.frontend.source j "storage type mismatch" - -private partial def parseExpr (ctx : ParseCtx) {Γ : Sol.Ctx} (sc : Scope Γ) (t : Sol.Ty) - (j : Json) : MetaM (Sol.Expr ctx.L ctx.F Γ t) := do - match ← nodeKind j with - | "Identifier" => - let rid ← int (← field j "referencedDeclaration") - if rid < 0 then failAt ctx.frontend.source j "unresolved builtin identifier" - let id := rid.toNat - if let some v := sc.find? id then - match v.1 with - | .pair sid => - let some expected := pairTypeName ctx.frontend sid | failAt ctx.frontend.source j "unsupported value type" - requireType ctx.frontend j expected - | _ => requireType ctx.frontend j (typeName v.1) - if h : v.1 = t then pure (.var (h ▸ v.2)) else failAt ctx.frontend.source j "local type mismatch" - else if ctx.frontend.fields.any (fun f => f.id == id && f.structId.isSome) then - match t with - | .pair sid => - let some expected := pairTypeName ctx.frontend sid | failAt ctx.frontend.source j "unsupported value type" - requireType ctx.frontend j expected - loadPair ctx id sid j - | _ => failAt ctx.frontend.source j "storage type mismatch" - else if let some info := ctx.frontend.fields.find? (·.id == id) then - match info.sty, t with - | .scalar, .uint => - requireType ctx.frontend j "uint256" - pure (.load (← svar ctx.frontend ctx.L id .scalar j)) - | .addr, .addr => - requireType ctx.frontend j "address" - pure (.loadAddr (← svar ctx.frontend ctx.L id .addr j)) - | .mapping, _ => failAt ctx.frontend.source j "mapping requires index access" - | _, _ => failAt ctx.frontend.source j "storage type mismatch" - else if ctx.frontend.opaqueEntries.any (·.id == id) then - failAt ctx.frontend.source j "opaque field" - else failAt ctx.frontend.source j "unresolved declaration reference" - | "MemberAccess" => - let base ← field j "expression" - let member ← str (← field j "memberName") - if member == "sender" && (← nodeKind base) == "Identifier" && - (← str (← field base "name")) == "msg" && - (← int (← field base "referencedDeclaration")) == -15 && - (field? j "referencedDeclaration").all fun v => v.isNull then - requireType ctx.frontend base "msg" - requireType ctx.frontend j "address" - match t with - | .addr => pure .sender - | .uint => failAt ctx.frontend.source j "address expression in uint256 position" - | .pair _ => failAt ctx.frontend.source j "address expression in uint256 position" - else - unless isPairTypeString ctx.frontend (← typeString base) do - failAt ctx.frontend.source j "only builtin msg.sender supported" - let some info := lookupStructByTypeString ctx.frontend (← typeString base) - | failAt ctx.frontend.source j "using an undeclared struct" - let some idx := info.members.findIdx? (·.name == member) - | failAt ctx.frontend.source j "unresolved declaration reference" - let baseE ← parseExpr ctx sc (.pair info.id) base - match idx, t with - | 0, .uint => - requireType ctx.frontend j "uint256" - pure (.fst baseE) - | 1, .addr => - requireType ctx.frontend j "address" - pure (.snd baseE) - | _, _ => failAt ctx.frontend.source j "local type mismatch" - | "IndexAccess" => - let base ← field j "baseExpression" - needAt ctx.frontend.source j ((← nodeKind base) == "Identifier") "unsupported index base" - let rid ← int (← field base "referencedDeclaration") - if rid < 0 then failAt ctx.frontend.source j "unsupported index base" - if ctx.frontend.opaqueEntries.any (·.id == rid.toNat) then failAt ctx.frontend.source j "opaque field" - let some info := ctx.frontend.fields.find? (·.id == rid.toNat) - | failAt ctx.frontend.source j "unsupported index base" - needAt ctx.frontend.source j (info.sty == .mapping) "unsupported index base" - requireType ctx.frontend base "mapping(address => uint256)" - let index ← field j "indexExpression" - requireType ctx.frontend index "address" - requireType ctx.frontend j "uint256" - let key ← parseExpr ctx sc .addr index - match t with - | .uint => pure (.index (← svar ctx.frontend ctx.L info.id .mapping j) key) - | .addr => failAt ctx.frontend.source j "uint256 expression in address position" - | .pair _ => failAt ctx.frontend.source j "uint256 expression in address position" - | "Literal" => - let value ← str (← field j "value") - let some n := value.toNat? | failAt ctx.frontend.source j "unsupported literal" - needAt ctx.frontend.source j ((← str (← field j "kind")) == "number" && - (field? j "subdenomination").all Json.isNull && n < 2^256) "unsupported literal" - needAt ctx.frontend.source j ((← typeString j).startsWith "int_const ") "unsupported literal type" - match t with - | .uint => pure (.lit n) - | .addr => failAt ctx.frontend.source j "uint256 expression in address position" - | .pair _ => failAt ctx.frontend.source j "uint256 expression in address position" - | "BinaryOperation" => - let op ← str (← field j "operator") - needAt ctx.frontend.source j (op == "+" || op == "-") "unsupported binary operation/types" - requireType ctx.frontend j "uint256" - let left ← field j "leftExpression" - let right ← field j "rightExpression" - requireType ctx.frontend left "uint256"; requireType ctx.frontend right "uint256" - let a ← parseExpr ctx sc .uint left - let b ← parseExpr ctx sc .uint right - let aop : Sol.ArithOp := if op == "+" then .add else .sub - match t with - | .uint => pure (.arith aop a b) - | .addr => failAt ctx.frontend.source j "uint256 expression in address position" - | .pair _ => failAt ctx.frontend.source j "uint256 expression in address position" - | "FunctionCall" => - if (← str (← field j "kind")) == "structConstructorCall" then - let some info := lookupStructByTypeString ctx.frontend (← typeString j) - | failAt ctx.frontend.source j "using an undeclared struct" - needAt ctx.frontend.source j (t == .pair info.id) "unsupported value type" - requireType ctx.frontend j (pairTypeNameOf info) - let argsJ ← arr (← field j "arguments") - let names ← arr (← field j "names") - needAt ctx.frontend.source j (argsJ.size == 2) "argument count mismatch" - let amountName := - match info.members with - | m0 :: _ => m0.name - | [] => "" - let whoName := - match info.members with - | _ :: m1 :: _ => m1.name - | _ => "" - let e : Sol.Expr ctx.L ctx.F Γ (.pair info.id) ← - if names.isEmpty then - let a ← parseExpr ctx sc .uint argsJ[0]! - let b ← parseExpr ctx sc .addr argsJ[1]! - pure (.pair a b) - else do - needAt ctx.frontend.source j (names.size == 2) "argument count mismatch" - let some iAmt := names.findIdx? (fun n => n.getStr?.toOption == some amountName) - | failAt ctx.frontend.source j "unresolved declaration reference" - let some iWho := names.findIdx? (fun n => n.getStr?.toOption == some whoName) - | failAt ctx.frontend.source j "unresolved declaration reference" - needAt ctx.frontend.source j (iAmt != iWho) "unresolved declaration reference" - if iAmt == 0 && iWho == 1 then - let a ← parseExpr ctx sc .uint argsJ[0]! - let b ← parseExpr ctx sc .addr argsJ[1]! - pure (.pair a b) - else if iAmt == 1 && iWho == 0 then - let b ← parseExpr ctx sc .addr argsJ[0]! - let a ← parseExpr ctx sc .uint argsJ[1]! - pure (.pairRev b a) - else failAt ctx.frontend.source j "unresolved declaration reference" - if h : t = .pair info.id then pure (h ▸ e) else failAt ctx.frontend.source j "local type mismatch" - else do - let resolved ← resolveCall ctx.frontend ctx.current ctx.superFrom j - needAt ctx.frontend.source j resolved.viewOrPure - "effectful internal call in expression position" - needAt ctx.frontend.source j (resolved.rets == [t]) "function does not return the expected type" - let ⟨σ, fv⟩ ← fvarOf ctx resolved j - let argsJ := (← arr (← field j "arguments")).toList - let args ← parseArgs ctx sc resolved.paramTys argsJ j - if h : t = .uint then - requireType ctx.frontend j "uint256" - if hσ : σ = ⟨resolved.paramTys.reverse, [.uint]⟩ then - let e : Sol.Expr ctx.L ctx.F Γ .uint := .call (hσ ▸ fv) args - pure (h ▸ e) - else failAt ctx.frontend.source j "function signature mismatch" - else if h : t = .addr then - requireType ctx.frontend j "address" - if hσ : σ = ⟨resolved.paramTys.reverse, [.addr]⟩ then - let e : Sol.Expr ctx.L ctx.F Γ .addr := .call (hσ ▸ fv) args - pure (h ▸ e) - else failAt ctx.frontend.source j "function signature mismatch" - else - match t with - | .pair sid => - let some expected := pairTypeName ctx.frontend sid | failAt ctx.frontend.source j "unsupported value type" - requireType ctx.frontend j expected - if hσ : σ = ⟨resolved.paramTys.reverse, [.pair sid]⟩ then - let e : Sol.Expr ctx.L ctx.F Γ (.pair sid) := .call (hσ ▸ fv) args - if h : t = .pair sid then pure (h ▸ e) - else failAt ctx.frontend.source j "function signature mismatch" - else failAt ctx.frontend.source j "function signature mismatch" - | _ => failAt ctx.frontend.source j "function signature mismatch" - | _ => failAt ctx.frontend.source j "unsupported expression" - -private partial def parseArgs (ctx : ParseCtx) {Γ : Sol.Ctx} (sc : Scope Γ) (ts : List Sol.Ty) - (js : List Json) (origin : Json) : MetaM (Sol.Args ctx.L ctx.F Γ ts) := do - match ts, js with - | [], [] => pure .nil - | t :: ts, j :: js => - let e ← parseExpr ctx sc t j - let rest ← parseArgs ctx sc ts js origin - pure (.cons e rest) - | _, _ => failAt ctx.frontend.source origin "argument count mismatch" - -private partial def parseLValue (ctx : ParseCtx) {Γ : Sol.Ctx} (sc : Scope Γ) (j : Json) : - MetaM (Sol.LVal ctx.L ctx.F Γ) := do - match ← nodeKind j with - | "Identifier" => - let rid ← int (← field j "referencedDeclaration") - if rid < 0 then failAt ctx.frontend.source j "unsupported storage lvalue" - if ctx.frontend.opaqueEntries.any (·.id == rid.toNat) then failAt ctx.frontend.source j "opaque field" - let some info := ctx.frontend.fields.find? (·.id == rid.toNat) - | failAt ctx.frontend.source j "unsupported storage lvalue" - needAt ctx.frontend.source j (info.sty == .scalar) "mapping requires index access" - requireType ctx.frontend j "uint256" - pure (.scalar (← svar ctx.frontend ctx.L info.id .scalar j)) - | "IndexAccess" => - let base ← field j "baseExpression" - needAt ctx.frontend.source j ((← nodeKind base) == "Identifier") "unsupported index base" - let rid ← int (← field base "referencedDeclaration") - if rid < 0 then failAt ctx.frontend.source j "unsupported index base" - if ctx.frontend.opaqueEntries.any (·.id == rid.toNat) then failAt ctx.frontend.source j "opaque field" - let some info := ctx.frontend.fields.find? (·.id == rid.toNat) - | failAt ctx.frontend.source j "unsupported index base" - needAt ctx.frontend.source j (info.sty == .mapping) "unsupported index base" - requireType ctx.frontend base "mapping(address => uint256)" - let index ← field j "indexExpression" - requireType ctx.frontend index "address" - requireType ctx.frontend j "uint256" - let key ← parseExpr ctx sc .addr index - pure (.mapping (← svar ctx.frontend ctx.L info.id .mapping j) key) - | _ => failAt ctx.frontend.source j "only storage assignment supported" - -private partial def assignStructMembers (ctx : ParseCtx) {Γ : Sol.Ctx} - (id : Nat) (sid : Nat) (rhs : Sol.Expr ctx.L ctx.F Γ (.pair sid)) - (rest : Sol.Stmt ctx.L ctx.F Γ r) - (j : Json) : MetaM (Sol.Stmt ctx.L ctx.F Γ r) := do - let members := (List.range ctx.frontend.fields.length).zip ctx.frontend.fields |>.filter (fun p => p.2.id == id) - match members with - | [(p0, m0), (p1, m1)] => - needAt ctx.frontend.source j (m0.sty == .scalar && m1.sty == .addr) "storage type mismatch" - let a ← svarAt ctx.frontend ctx.L p0 .scalar j - let b ← svarAt ctx.frontend ctx.L p1 .addr j - let shifted := rest.shift (.pair sid) 0 - let inner : Sol.Stmt ctx.L ctx.F ((.pair sid) :: Γ) r := - .assign (.scalar a) .set (.fst (.var .here)) - (.assignAddr b (.snd (.var .here)) shifted) - pure (.local_ rhs inner) - | _ => failAt ctx.frontend.source j "storage type mismatch" - -private partial def parseStmts (ctx : ParseCtx) {Γ : Sol.Ctx} (sc : Scope Γ) (r : Sol.Ret) - (namedReturn : Option Nat) (nodes : List Json) : MetaM (Sol.Stmt ctx.L ctx.F Γ r) := do - match nodes with - | [] => - match r with - | [] => pure .done - | [t] => - match namedReturn with - | some id => - let some v := sc.find? id | throwError "missing named return" - if h : v.1 = t then pure (.ret (.var (h ▸ v.2))) - else throwError "named return type mismatch" - | none => throwError "missing terminal return" - | _ => throwError "missing terminal return" - | node :: rest => - match ← nodeKind node with - | "PlaceholderStatement" => failAt ctx.frontend.source node "modifier with two placeholders" - | "ExpressionStatement" => - let expr ← field node "expression" - match ← nodeKind expr with - | "Assignment" => - let op ← str (← field expr "operator") - let lhs ← field expr "leftHandSide" - if (← nodeKind lhs) == "Identifier" then - let rid ← int (← field lhs "referencedDeclaration") - if rid >= 0 && ctx.frontend.opaqueEntries.any (·.id == rid.toNat) then - failAt ctx.frontend.source node "opaque field" - if rid >= 0 then - if ctx.frontend.fields.any (fun f => f.id == rid.toNat && f.structId.isSome) then - needAt ctx.frontend.source expr (op == "=") "unsupported assignment" - let some info := lookupStructByTypeString ctx.frontend (← typeString lhs) - | failAt ctx.frontend.source expr "using an undeclared struct" - let rhs ← parseExpr ctx sc (.pair info.id) (← field expr "rightHandSide") - let restS ← parseStmts ctx sc r namedReturn rest - return (← assignStructMembers ctx rid.toNat info.id rhs restS lhs) - if let some info := ctx.frontend.fields.find? (·.id == rid.toNat) then - if info.sty == .addr then - needAt ctx.frontend.source expr (op == "=") "unsupported assignment" - requireType ctx.frontend expr "address" - let rhs ← parseExpr ctx sc .addr (← field expr "rightHandSide") - let sv ← svar ctx.frontend ctx.L info.id .addr lhs - return .assignAddr sv rhs (← parseStmts ctx sc r namedReturn rest) - requireType ctx.frontend expr "uint256" - needAt ctx.frontend.source expr (op == "=" || op == "+=" || op == "-=") "unsupported assignment" - let lv ← parseLValue ctx sc lhs - let rhs ← parseExpr ctx sc .uint (← field expr "rightHandSide") - let aop : Sol.AssignOp := if op == "=" then .set else if op == "+=" then .add else .sub - pure (.assign lv aop rhs (← parseStmts ctx sc r namedReturn rest)) - | "FunctionCall" => - requireType ctx.frontend expr "tuple()" - let resolved ← resolveCall ctx.frontend ctx.current ctx.superFrom expr - needAt ctx.frontend.source expr resolved.rets.isEmpty "function does not return the expected type" - let ⟨σ, fv⟩ ← fvarOf ctx resolved expr - let argsJ := (← arr (← field expr "arguments")).toList - let args ← parseArgs ctx sc resolved.paramTys argsJ expr - let restS ← parseStmts ctx sc r namedReturn rest - if h : σ = ⟨resolved.paramTys.reverse, []⟩ then - pure (.callStmt (h ▸ fv) args restS) - else failAt ctx.frontend.source expr "function signature mismatch" - | _ => failAt ctx.frontend.source node "unsupported expression statement" - | "VariableDeclarationStatement" => - let declarations ← arr (← field node "declarations") - needAt ctx.frontend.source node (declarations.size == 1) "unsupported locals" - let declaration := declarations[0]! - needAt ctx.frontend.source declaration (!(← bool (← field declaration "stateVariable")) && - !(← bool (← field declaration "constant")) && - (← str (← field declaration "mutability")) == "mutable" && - (← str (← field declaration "storageLocation")) == "default" && - (← str (← field declaration "visibility")) == "internal") "unsupported local declaration" - requireType ctx.frontend declaration "uint256" - let id ← nodeId declaration - let _ ← identifier ctx.frontend.source declaration - let value ← parseExpr ctx sc .uint (← field node "initialValue") - pure (.local_ value (← parseStmts ctx (sc.push id .uint) r namedReturn rest)) - | "IfStatement" => - let falseBody := field? node "falseBody" - let trueBody ← field node "trueBody" - let revertNode ← match ← nodeKind trueBody with - | "Block" => - let statements ← arr (← field trueBody "statements") - needAt ctx.frontend.source node (statements.size == 1 && - (← nodeKind statements[0]!) == "RevertStatement") "only if/revert guard supported" - pure statements[0]! - | "RevertStatement" => pure trueBody - | _ => failAt ctx.frontend.source node "only if/revert guard supported" - needAt ctx.frontend.source node (falseBody.all Json.isNull) "only if/revert guard supported" - let call ← field revertNode "errorCall" - let callee ← field call "expression" - let rid ← int (← field callee "referencedDeclaration") - let some errorName := if rid < 0 then none else ctx.frontend.errors.lookup rid.toNat - | failAt ctx.frontend.source node "unsupported revert" - needAt ctx.frontend.source node ((← nodeKind callee) == "Identifier" && - (← arr (← field call "arguments")).isEmpty) "unsupported revert" - let condition ← field node "condition" - needAt ctx.frontend.source condition ((← nodeKind condition) == "BinaryOperation") - "only uint256 comparison guard supported" - requireType ctx.frontend condition "bool" - let op ← str (← field condition "operator") - let left ← field condition "leftExpression" - let right ← field condition "rightExpression" - if op == "<" then - let commonType ← field condition "commonType" - needAt ctx.frontend.source condition ((← str (← field commonType "typeString")) == "uint256" && - (← str (← field commonType "typeIdentifier")) == "t_uint256") "unsupported comparison type" - requireUintish ctx.frontend left; requireUintish ctx.frontend right - let a ← parseExpr ctx sc .uint left - let b ← parseExpr ctx sc .uint right - pure (.guard a b (errorName ++ "()") (← parseStmts ctx sc r namedReturn rest)) - else if op == "!=" then - let commonType ← field condition "commonType" - needAt ctx.frontend.source condition ((← str (← field commonType "typeString")) == "address") - "unsupported comparison type" - requireType ctx.frontend left "address"; requireType ctx.frontend right "address" - let a ← parseExpr ctx sc .addr left - let b ← parseExpr ctx sc .addr right - pure (.guardAddrNe a b (errorName ++ "()") (← parseStmts ctx sc r namedReturn rest)) - else failAt ctx.frontend.source condition "only uint256 comparison guard supported" - | "Return" => - needAt ctx.frontend.source node rest.isEmpty "only terminal scalar return" - match r with - | [t] => - match field? node "expression" with - | some expr => - needAt ctx.frontend.source node (!expr.isNull) "only terminal scalar return" - let e ← parseExpr ctx sc t expr - pure (.ret e) - | none => failAt ctx.frontend.source node "only terminal scalar return" - | [] => - needAt ctx.frontend.source node ((field? node "expression").all fun e => e.isNull) - "only terminal scalar return" - pure .done - | _ => failAt ctx.frontend.source node "only terminal scalar return" - | _ => failAt ctx.frontend.source node "unsupported statement" -end - -private def checkCollisions (ns : Name) (frontend : Frontend) (order : List FnInfo) : MetaM Unit := do - let mut names := #[ - ns ++ `sourceDigest, ns ++ `Storage, ns ++ `view, ns ++ `step, - ns ++ `Storage ++ `mk, ns ++ `opaqueFields, ns ++ `registeredSources] - for f in frontend.fields do - names := names.push (ns ++ Name.mkSimple f.name) - names := names.push (ns ++ `Storage ++ Name.mkSimple f.var) - if let some getter := f.getter then names := names.push (ns ++ Name.mkSimple getter) - for fn in order do - names := names.push (ns ++ Name.mkSimple fn.leanIdent) - for i in [:names.size] do - if (← getEnv).contains names[i]! || (names.extract 0 i).contains names[i]! then - throwError "declaration collision: {names[i]!}" - -private def storageStructure (alias : Name) (frontend : Frontend) : CommandElabM Syntax := do - let declId := mkIdent (alias ++ `Storage) - let binders : TSyntaxArray ``Parser.Command.structExplicitBinder ← - frontend.fields.toArray.mapM fun f => do - let fieldId := mkIdent (Name.mkSimple f.var) - let ty : Term ← match f.sty with - | .mapping => `(Verity.Address → Verity.Uint256) - | .addr => `(Verity.Address) - | .scalar => `(Verity.Uint256) - `(Parser.Command.structExplicitBinder| ($fieldId:ident : $ty)) - `(command| structure $declId where $binders:structExplicitBinder*) - -private def mkEntryDisjunct (s s' : Expr) (name : Name) (binders : List (Name × Expr)) : MetaM Expr := - go binders #[] -where - go : List (Name × Expr) → Array Expr → MetaM Expr - | [], xs => do - let run ← mkAppM ``Verity.Contract.run #[mkAppN (mkConst name) xs, s] - let snd ← mkAppM ``Verity.ContractResult.snd #[run] - mkEq s' snd - | (n, dom) :: rest, xs => - withLocalDeclD n dom fun x => do - let inner ← go rest (xs.push x) - mkAppOptM ``Exists #[some dom, some (← mkLambdaFVars #[x] inner)] - -private def envApply (σ : Sol.Sig) (fn : Expr) : MetaM Expr := do - let envTy ← mkAppM ``Sol.Env #[toExpr σ.params] - withLocalDeclD `env envTy fun env => do - let mut app := fn - let n := σ.params.length - for i in [:n] do - let dbIndex := n - 1 - i - let some v := Sol.Var.ofIndex σ.params dbIndex | throwError "envApply: missing binder" - let getter ← mkAppOptM ``Sol.Env.get - #[some (toExpr σ.params), some (toExpr v.1), some env, some (toExpr v.2)] - app := mkApp app getter - mkLambdaFVars #[env] app - -private def fnsTerm : (handles : List (Sol.Sig × Expr)) → MetaM Expr - | [] => pure (mkConst ``Sol.FnEnv.nil) - | (σ, h) :: rest => do - let restE ← fnsTerm rest - let apply ← envApply σ h - let envTy ← mkAppM ``Sol.Env #[toExpr σ.params] - let retTy ← mkAppM ``Sol.Ret.denote #[toExpr σ.rets] - let contractTy ← mkAppM ``Verity.Contract #[retTy] - let applyTy ← mkArrow envTy contractTy - let apply ← mkExpectedTypeHint apply applyTy - let tail : Sol.Fns := rest.map (·.1) - -- `FnEnv.cons` implicits are `{F : Fns}` then `{σ : Sig}`. - pure (mkAppN (mkConst ``Sol.FnEnv.cons) #[toExpr tail, toExpr σ, apply, restE]) - -private partial def wrapModifierParts (ctx : ParseCtx) {Γ : Sol.Ctx} (sc : Scope Γ) - (pre post : List Json) (inner : Sol.Stmt ctx.L ctx.F Γ r) : - MetaM (Sol.Stmt ctx.L ctx.F Γ r) := do - match pre with - | [] => - let postS ← parseStmts ctx sc [] none post - pure (.block inner postS) - | node :: restPre => - if (← nodeKind node) == "Return" then - failAt ctx.frontend.source node "modifier prelude return skips function body" - if (← nodeKind node) == "VariableDeclarationStatement" then - let declarations ← arr (← field node "declarations") - needAt ctx.frontend.source node (declarations.size == 1) "unsupported locals" - let declaration := declarations[0]! - needAt ctx.frontend.source declaration (!(← bool (← field declaration "stateVariable")) && - !(← bool (← field declaration "constant")) && - (← str (← field declaration "mutability")) == "mutable" && - (← str (← field declaration "storageLocation")) == "default" && - (← str (← field declaration "visibility")) == "internal") "unsupported local declaration" - requireType ctx.frontend declaration "uint256" - let id ← nodeId declaration - let _ ← identifier ctx.frontend.source declaration - let value ← parseExpr ctx sc .uint (← field node "initialValue") - let rest ← wrapModifierParts ctx (sc.push id .uint) restPre post (inner.shift .uint 0) - pure (.local_ value rest) - else - let head ← parseStmts ctx sc [] none [node] - let rest ← wrapModifierParts ctx sc restPre post inner - pure (.seq head rest) - -private def wrapModifiers (ctx : ParseCtx) {Γ : Sol.Ctx} (sc : Scope Γ) - (invs : List Json) (body : Sol.Stmt ctx.L ctx.F Γ r) : - MetaM (Sol.Stmt ctx.L ctx.F Γ r) := do - match invs with - | [] => pure body - | inv :: rest => - needAt ctx.frontend.source inv ((← str (← field inv "kind")) == "modifierInvocation") - "unsupported modifier" - if let some args := field? inv "arguments" then - unless args.isNull do - needAt ctx.frontend.source inv ((← arr args).isEmpty) "modifier arguments" - let nameNode ← field inv "modifierName" - let rid ← int (← field nameNode "referencedDeclaration") - needAt ctx.frontend.source inv (rid >= 0) "unresolved modifier" - let some m := lookupMod ctx.frontend rid.toNat | failAt ctx.frontend.source inv "unresolved modifier" - let mbody := field? m.node "body" |>.getD Json.null - let nph := countPlaceholders mbody - if nph > 1 then failAt ctx.frontend.source m.node "modifier with two placeholders" - if nph == 0 then failAt ctx.frontend.source m.node "modifier missing placeholder" - let stmts := (field? mbody "statements").bind (·.getArr?.toOption) |>.getD #[] - let (pre, post) ← splitModifierBody stmts.toList - let inner ← wrapModifiers ctx sc rest body - for s in pre do - if (← nodeKind s) == "Return" then - failAt ctx.frontend.source s "modifier prelude return skips function body" - let mctx := { ctx with superFrom := m.contractId } - if ← pre.anyM fun s => (· == "VariableDeclarationStatement") <$> nodeKind s then - wrapModifierParts mctx sc pre post inner - else - let preS ← parseStmts mctx sc [] none pre - let postS ← parseStmts mctx sc [] none post - pure (.seq preS (.block inner postS)) - -private def parseBody (ctx : ParseCtx) (fn : FnInfo) : MetaM Expr := do - let Γ : Sol.Ctx := fn.paramIds.reverse.map fun p => p.2.1 - let sc0 := scopeOf fn.paramIds Γ - let statements := - match field? fn.node "body" with - | some body => (field? body "statements").bind (·.getArr?.toOption) |>.getD #[] - | none => #[] - let invs := (← fnModifierInvocations fn).toList - match fn.namedReturn with - | some (id, ty, _) => - let sc := sc0.push id ty - let defaultE : Sol.Expr ctx.L ctx.F Γ ty := - match ty with - | .uint => .lit 0 - | .addr => .zero - | .pair _ => .pair (.lit 0) .zero - let inner ← parseStmts ctx sc fn.rets (some id) statements.toList - let wrapped : Sol.Stmt ctx.L ctx.F Γ fn.rets := .local_ defaultE inner - let withMods ← wrapModifiers ctx sc0 invs wrapped - pure (toExpr withMods) - | none => - let body ← parseStmts ctx sc0 fn.rets none statements.toList - let withMods ← wrapModifiers ctx sc0 invs body - pure (toExpr withMods) - -private def importFrontend (ns : Name) (frontend : Frontend) : MetaM Unit := do - let L := layoutOf frontend.fields - let order ← topoSort frontend - let mut registered : List (FieldInfo × Expr) := [] - for f in frontend.fields do - let ty ← match f.sty with - | .mapping => mkArrow address uint - | .addr => pure address - | .scalar => pure uint - let slot ← mkAppOptM ``Verity.StorageSlot.mk #[some ty, some (mkNatLit f.slot)] - let name := ns ++ Name.mkSimple f.name - register name slot - registered := registered ++ [(f, mkConst name)] - let slotsE ← slotsTerm (registered.map fun h => (h.1.sty, h.2)) - let state := mkConst ``Verity.ContractState - let storageView := mkConst (ns ++ `Storage) - let view ← withLocalDeclD `s state fun s => do - let mut args : Array Expr := #[] - for h in registered do - let slot ← mkAppM ``Verity.StorageSlot.slot #[h.2] - match h.1.sty with - | .mapping => - let reader ← withLocalDeclD `k address fun k => - mkLambdaFVars #[k] (mkAppN (mkConst ``Verity.ContractState.readMap) #[s, slot, k]) - args := args.push reader - | .addr => - args := args.push (mkAppN (mkConst ``Verity.ContractState.readAddrSlot) #[s, slot]) - | .scalar => - args := args.push (mkAppN (mkConst ``Verity.ContractState.readSlot) #[s, slot]) - mkLambdaFVars #[s] (← mkAppM (ns ++ `Storage ++ `mk) args) - register (ns ++ `view) view (some (← mkArrow state storageView)) - let opaqueVal : List (String × Nat × String) := - frontend.opaqueEntries.map fun o => (o.label, o.slot, o.typeLabel) - register (ns ++ `opaqueFields) (toExpr opaqueVal) (simp := false) - register (ns ++ `registeredSources) (toExpr registeredSources) (simp := false) - let mut getterEntries : Array (Name × List (Name × Expr)) := #[] - for h in registered do - if let some getter := h.1.getter then - let gname := ns ++ Name.mkSimple getter - match h.1.structId, h.1.memberIndex with - | some _, some 0 => - let members := registered.filter (fun g => g.1.id == h.1.id && g.1.structId.isSome) - needAt frontend.source frontend.ast (members.length == 2) "storage type mismatch" - let some amountH := members.find? (fun g => g.1.memberIndex == some 0) - | throwError "unresolved struct getter" - let some whoH := members.find? (fun g => g.1.memberIndex == some 1) - | throwError "unresolved struct getter" - let amount ← mkAppM ``Verity.getStorage #[amountH.2] - let who ← mkAppM ``Verity.getStorageAddr #[whoH.2] - let value ← mkAppM ``Sol.nonpayable - #[← mkAppM ``Verity.bind #[amount, ← withLocalDeclD `x uint fun x => do - mkLambdaFVars #[x] (← mkAppM ``Verity.bind #[who, ← withLocalDeclD `y address fun y => do - mkLambdaFVars #[y] (← mkAppM ``Verity.pure #[← mkAppM ``Prod.mk #[x, y]])])]] - register gname value - getterEntries := getterEntries.push (gname, []) - | some _, some _ => pure () - | _, _ => - match h.1.sty with - | .mapping => - let value ← withLocalDeclD `account address fun account => do - let getterFn ← mkAppM ``Verity.getMapping #[h.2, account] - mkLambdaFVars #[account] (← mkAppM ``Sol.nonpayable #[getterFn]) - register gname value - getterEntries := getterEntries.push (gname, [(`account, address)]) - | .addr => - let value ← mkAppM ``Sol.nonpayable #[← mkAppM ``Verity.getStorageAddr #[h.2]] - register gname value - getterEntries := getterEntries.push (gname, []) - | .scalar => - let value ← mkAppM ``Sol.nonpayable #[← mkAppM ``Verity.getStorage #[h.2]] - register gname value - getterEntries := getterEntries.push (gname, []) - let mut fnHandles : List (Sol.Sig × Expr) := [] - let mut fnInfos : List FnInfo := [] - let mut functionEntries : Array (Name × List (Name × Expr)) := #[] - for fn in order do - let F : Sol.Fns := fnHandles.map (·.1) - let fnsE ← fnsTerm fnHandles - let ctx : ParseCtx := { frontend, L, F, registered := fnInfos, current := fn, superFrom := fn.contractId } - let Γ : Sol.Ctx := fn.paramIds.reverse.map fun p => p.2.1 - let bodyE ← parseBody ctx fn - let meaningHead := - if fn.isEntry then mkConst ``Sol.Fn.meaning else mkConst ``Sol.Fn.bodyMeaning - let value ← withParams fn.paramIds [] fun xs => do - let envE ← envTerm xs - pure (mkAppN meaningHead - #[toExpr L, toExpr F, toExpr Γ, toExpr fn.rets, bodyE, slotsE, fnsE, envE]) - let mut type := ← mkAppM ``Verity.Contract #[resultType fn.rets] - for p in fn.paramIds.reverse do - type ← mkArrow (valueType p.2.1) type - let fname := ns ++ Name.mkSimple fn.leanIdent - register fname value (some type) - let binders := fn.paramIds.map fun p => (Name.mkSimple p.2.2, valueType p.2.1) - if fn.isEntry then - if let some top := mostDerived frontend fn.name fn.paramTys then - if top.id == fn.id then - functionEntries := functionEntries.push (fname, binders) - fnHandles := fnHandles ++ [(fn.sig, mkConst fname)] - fnInfos := fnInfos ++ [fn] - -- Entry-point order: target public/external in source order, then each base - -- in linearization order, then getters in layout order. - let mut orderedEntries : Array (Name × List (Name × Expr)) := #[] - for cid in frontend.linearization do - for fn in frontend.functions do - if fn.contractId == cid && fn.isEntry then - if let some top := mostDerived frontend fn.name fn.paramTys then - if top.id == fn.id then - let fname := ns ++ Name.mkSimple fn.leanIdent - if let some entry := functionEntries.find? (·.1 == fname) then - unless orderedEntries.any (fun e => e.1 == fname) do - orderedEntries := orderedEntries.push entry - let entries := orderedEntries ++ getterEntries - let step ← withLocalDeclD `s state fun s => - withLocalDeclD `s' state fun s' => do - let mut disjuncts : Array Expr := #[] - for (ename, binders) in entries do - disjuncts := disjuncts.push (← mkEntryDisjunct s s' ename binders) - if disjuncts.isEmpty then throwError "imported contract has no entry points" - let mut body := disjuncts.back! - for i in (List.range (disjuncts.size - 1)).reverse do - body := mkApp2 (mkConst ``Or) disjuncts[i]! body - mkLambdaFVars #[s, s'] body - let stepType ← mkArrow state (← mkArrow state (mkSort 0)) - register (ns ++ `step) step (some stepType) (simp := false) - register (ns ++ `sourceDigest) (mkStrLit frontend.digest) (simp := false) - -private def compileFrontend (root source : System.FilePath) (targetName? : Option String) : MetaM Frontend := do - let canonicalRoot ← IO.FS.realPath root - let canonicalSource ← IO.FS.realPath source - unless canonicalSource.toString.startsWith (canonicalRoot.toString ++ "/") do - throwError "source outside package" - let mut matched : Option (String × String) := none - for pair in registeredSources do - let expected ← IO.FS.realPath (canonicalRoot / pair.2) - if canonicalSource == expected then matched := some pair - let some (logicalPath, _) := matched | throwError "unregistered source or source outside package" - let compiler := canonicalRoot / ".lake/solidity-import/solc" - verifyCompiler compiler - let versionOut ← IO.Process.output { cmd := compiler.toString, args := #["--version"] } - unless versionOut.exitCode == 0 && - acceptedSolcVersionOutputs.contains versionOut.stdout.trimAscii.toString do - throwError "compiler version mismatch" - verifyCompiler compiler - let sourceBytes ← IO.FS.readBinFile canonicalSource - let sourceText ← match String.fromUTF8? sourceBytes with - | some text => pure text - | none => throwError "Solidity source is not UTF-8" - let settings := Json.mkObj [ - ("optimizer", Json.mkObj [("enabled", false)]), - ("viaIR", false), ("evmVersion", "cancun"), ("remappings", Json.arr #[]), - ("outputSelection", Json.mkObj [("*", Json.mkObj [ - ("", Json.arr #["ast"]), ("*", Json.arr #["storageLayout"])])])] - let input := Json.mkObj [("language", "Solidity"), - ("sources", Json.mkObj [(logicalPath, Json.mkObj [("content", sourceText)])]), - ("settings", settings)] - let output ← IO.Process.output - { cmd := compiler.toString, args := #["--standard-json", "--no-import-callback"] } - (some input.compress) - unless output.exitCode == 0 do throwError "solc failed: {output.stderr}" - verifyCompiler compiler - let importerDir := canonicalRoot / "Contracts/VaultFromSolidity/Importer" - let importerText ← IO.FS.readFile (importerDir / "Importer.lean") - let syntaxText ← IO.FS.readFile (importerDir / "Syntax.lean") - let semanticsText ← IO.FS.readFile (importerDir / "Semantics.lean") - parseCompilerOutput canonicalSource logicalPath sourceBytes output.stdout - (importerText ++ syntaxText ++ semanticsText) targetName? - -syntax (name := solidityContract) "solidity_contract " ident " from " str : command -syntax (name := solidityContractNamed) "solidity_contract " ident " from " str " contract " str : command - -private def elabSolidityContractCore (alias : Name) (rel : String) (targetName? : Option String) - (_stx : Syntax) : CommandElabM Unit := do - let saved ← getEnv - try - if debug.skipKernelTC.get (← getOptions) then throwError "kernel checking must be enabled" - let authored ← IO.FS.realPath (← getFileName) - let source := authored.parent.getD "." / rel - let mut root := authored.parent.getD "." - while !(← (root / "lakefile.lean").pathExists) do - let some parent := root.parent | throwError "package root not found" - if parent == root then throwError "package root not found" - root := parent - let frontend ← liftTermElabM <| compileFrontend root source targetName? - let ns := (← getCurrNamespace) ++ alias - let order ← liftTermElabM <| topoSort frontend - liftTermElabM <| checkCollisions ns frontend order - let storageStx ← storageStructure alias frontend - withScope (fun scope => { scope with opts := Elab.async.set scope.opts false }) do - elabCommand storageStx - if (← get).messages.hasErrors then - throwError "storage view elaboration failed" - liftTermElabM <| withOptions (Elab.async.set · false) (importFrontend ns frontend) - catch e => - setEnv saved - throw e - -@[command_elab solidityContract] def elabSolidityContract : CommandElab := fun stx => do - elabSolidityContractCore stx[1].getId stx[3].isStrLit?.get! none stx - -@[command_elab solidityContractNamed] def elabSolidityContractNamed : CommandElab := fun stx => do - elabSolidityContractCore stx[1].getId stx[3].isStrLit?.get! stx[5].isStrLit? stx - -end SolidityImporter diff --git a/Contracts/VaultFromSolidity/Importer/Semantics.lean b/Contracts/VaultFromSolidity/Importer/Semantics.lean deleted file mode 100644 index 97229b4878..0000000000 --- a/Contracts/VaultFromSolidity/Importer/Semantics.lean +++ /dev/null @@ -1,244 +0,0 @@ -/- - What each accepted Solidity construct means, in Verity terms. - - This is the single definition of the translation: `Importer.lean` parses solc's - typed AST into the closed inductive in `Syntax.lean` and registers each entry - point as `Fn.meaning body slots fns env`. Nothing here inspects JSON, and nothing - here is generated source -- the registered value is a kernel-checked Lean term. - - Statement order matches the Solidity source order exactly, because the proofs - pin the write order the contract produces. Internal calls evaluate arguments - left to right, then run the callee body under the caller's `msg.sender` / - `msg.value`. `Expr.call` is `view`/`pure` only; effectful internals are - `Stmt.callStmt`. Pinned solc 0.8.x legacy codegen (`viaIR=false`) evaluates - an effectful call before the other operand / `+=` old-read, so modelling - those calls as left-to-right `Expr.call` would disagree with bytecode. - - Function modifiers are inlined at parse time; this file never sees `_`. - `Stmt.block inner post` runs `inner` and then `post`, so a function `return` - exits the block only and postludes still run. A revert anywhere aborts the - whole call. --/ - -import Contracts.VaultFromSolidity.Importer.Syntax -import Verity.Stdlib.Math -import Verity.Core.SolidityImportAttr - -namespace SolidityImporter.Sol - -open Lean - -/-- Two-member user struct as a product. Shared with the importer's Lean binder -type so `Env.cons` sees the same type as `Ty.pair.denote`. -/ -abbrev Pair := Verity.Core.Uint256 × Verity.Core.Address - -/-- `uint256` denotes Verity's `Uint256`, `address` its `Address`, two-member -user structs their product (the solc id is erased at denote). -/ -@[reducible] def Ty.denote : Ty → Type - | .uint => Verity.Core.Uint256 - | .addr => Verity.Core.Address - | .pair _ => Pair - -/-- A function body returns nothing, one value, or (later) a tuple. -/ -@[reducible] def Ret.denote : Ret → Type - | [] => Unit - | t :: [] => t.denote - | t :: ts => t.denote × Ret.denote ts - -/-- A state variable is a named `StorageSlot`, scalar, address, or `address`-keyed. -/ -@[reducible] def StorageTy.denote : StorageTy → Type - | .scalar => Verity.StorageSlot Verity.Core.Uint256 - | .mapping => Verity.StorageSlot (Verity.Core.Address → Verity.Core.Uint256) - | .addr => Verity.StorageSlot Verity.Core.Address - -/-- Parameter/local values, in the same de Bruijn order as `Ctx`. -/ -inductive Env : Ctx → Type where - | nil : Env [] - | cons : (x : t.denote) → (rest : Env Γ) → Env (t :: Γ) - -/-- The imported state-variable handles, in the same order as `Layout`. -/ -inductive Slots : Layout → Type where - | nil : Slots [] - | cons : (s : st.denote) → (rest : Slots L) → Slots (st :: L) - -/-- Declaration-order values of a call's arguments, before they are packed -into a de Bruijn `Env`. -/ -inductive Vals : List Ty → Type where - | nil : Vals [] - | cons : t.denote → Vals ts → Vals (t :: ts) - -/-- Already-registered function meanings: each takes a de Bruijn environment -of its parameters and returns a `Contract` of its result type. Slots are -baked into the registered Lean constants. -/ -inductive FnEnv : Fns → Type where - | nil : FnEnv [] - | cons : (Env σ.params → Verity.Contract (Ret.denote σ.rets)) → - FnEnv F → FnEnv (σ :: F) - -/-- The value bound at a variable. -/ -def Env.get : (env : Env Γ) → (v : Var Γ t) → t.denote - | .cons x _, .here => x - | .cons _ rest, .there v => Env.get rest v - -/-- The storage slot a state-variable handle refers to. -/ -def Slots.get : (slots : Slots L) → (s : SVar L st) → st.denote - | .cons s _, .here => s - | .cons _ rest, .there s => Slots.get rest s - -/-- The meaning of an already-registered function. -/ -def FnEnv.get : (fns : FnEnv F) → (f : FVar F σ) → - Env σ.params → Verity.Contract (Ret.denote σ.rets) - | .cons m _, .here => m - | .cons _ rest, .there f => FnEnv.get rest f - -/-- Pack declaration-order values into a de Bruijn environment by the same -`reverseAux` walk `List.reverse` uses, so the type is definitionally -`Env ts.reverse`. Last declared parameter is innermost. -/ -def reverseAuxEnv : {acc : Ctx} → (ts : List Ty) → Vals ts → Env acc → - Env (List.reverseAux ts acc) - | _, [], .nil, env => env - | acc, _ :: ts, .cons x vs, env => reverseAuxEnv (acc := _ :: acc) ts vs (.cons x env) - -/-- Pack declaration-order values into a de Bruijn environment. -/ -def envOfVals {ts : List Ty} (vs : Vals ts) : Env ts.reverse := - reverseAuxEnv ts vs .nil - -/-- Solidity 0.8 checked `+`/`-` on `uint256`: overflow/underflow reverts with -`Panic(0x11)` instead of wrapping. -/ -def checked (op : ArithOp) (a b : Verity.Core.Uint256) : Verity.Contract Verity.Core.Uint256 := - match op with - | .add => Verity.Stdlib.Math.requireSomeUint (Verity.Stdlib.Math.safeAdd a b) "Panic(0x11)" - | .sub => Verity.Stdlib.Math.requireSomeUint (Verity.Stdlib.Math.safeSub a b) "Panic(0x11)" - -mutual -/-- The meaning of an expression: a `Contract` producing its value. -/ -def Expr.meaning {L : Layout} {F : Fns} {Γ : Ctx} {t : Ty} - (e : Expr L F Γ t) (slots : Slots L) (fns : FnEnv F) (env : Env Γ) : - Verity.Contract t.denote := - match e with - | .var v => Verity.pure (Env.get env v) - | .sender => Verity.msgSender - | .zero => Verity.pure Verity.zeroAddress - | .lit n => Verity.pure (Verity.Core.Uint256.ofNat n) - | .load s => Verity.getStorage (Slots.get slots s) - | .loadAddr s => Verity.getStorageAddr (Slots.get slots s) - | .index m key => - Verity.bind (Expr.meaning key slots fns env) (fun a => - Verity.getMapping (Slots.get slots m) a) - | .arith op a b => - Verity.bind (Expr.meaning a slots fns env) (fun x => - Verity.bind (Expr.meaning b slots fns env) (fun y => checked op x y)) - | .pair a b => - Verity.bind (Expr.meaning a slots fns env) (fun x => - Verity.bind (Expr.meaning b slots fns env) (fun y => Verity.pure (x, y))) - | .pairRev a b => - Verity.bind (Expr.meaning a slots fns env) (fun y => - Verity.bind (Expr.meaning b slots fns env) (fun x => Verity.pure (x, y))) - | .fst e => - Verity.bind (Expr.meaning e slots fns env) (fun vs => Verity.pure vs.1) - | .snd e => - Verity.bind (Expr.meaning e slots fns env) (fun vs => Verity.pure vs.2) - | .call fvar args => - Verity.bind (Args.eval args slots fns env) (fun vals => - FnEnv.get fns fvar (envOfVals vals)) - -/-- Evaluate call arguments left to right in declaration order. -/ -def Args.eval {L : Layout} {F : Fns} {Γ : Ctx} {ts : List Ty} - (args : Args L F Γ ts) (slots : Slots L) (fns : FnEnv F) (env : Env Γ) : - Verity.Contract (Vals ts) := - match args with - | .nil => Verity.pure Vals.nil - | .cons e rest => - Verity.bind (Expr.meaning e slots fns env) (fun x => - Verity.bind (Args.eval rest slots fns env) (fun vs => - Verity.pure (Vals.cons x vs))) - -/-- `lhs op= rhs` followed by `k`, in source evaluation order: for `+=`/`-=` the -mapping key (if any) is read first, then the old value, then the right-hand side, -then the checked arithmetic, then the write, then the rest of the body. -/ -def assignWith {α : Type} (read : Verity.Contract Verity.Core.Uint256) - (write : Verity.Core.Uint256 → Verity.Contract Unit) : - AssignOp → Verity.Contract Verity.Core.Uint256 → Verity.Contract α → Verity.Contract α - | .set, rhs, k => Verity.bind rhs (fun v => Verity.bind (write v) (fun _ => k)) - | .add, rhs, k => - Verity.bind read (fun old => - Verity.bind rhs (fun v => - Verity.bind (checked .add old v) (fun w => Verity.bind (write w) (fun _ => k)))) - | .sub, rhs, k => - Verity.bind read (fun old => - Verity.bind rhs (fun v => - Verity.bind (checked .sub old v) (fun w => Verity.bind (write w) (fun _ => k)))) - -/-- The meaning of a statement: the rest of the body is its continuation. -/ -def Stmt.meaning {L : Layout} {F : Fns} {Γ : Ctx} {r : Ret} - (s : Stmt L F Γ r) (slots : Slots L) (fns : FnEnv F) (env : Env Γ) : - Verity.Contract (Ret.denote r) := - match s with - | .done => Verity.pure () - | .ret e => Expr.meaning e slots fns env - | .assign lv op rhs rest => - match lv with - | .scalar sv => - assignWith (Verity.getStorage (Slots.get slots sv)) - (fun v => Verity.setStorage (Slots.get slots sv) v) op - (Expr.meaning rhs slots fns env) (Stmt.meaning rest slots fns env) - | .mapping sv key => - Verity.bind (Expr.meaning key slots fns env) (fun a => - assignWith (Verity.getMapping (Slots.get slots sv) a) - (fun v => Verity.setMapping (Slots.get slots sv) a v) op - (Expr.meaning rhs slots fns env) (Stmt.meaning rest slots fns env)) - | .assignAddr sv rhs rest => - Verity.bind (Expr.meaning rhs slots fns env) (fun a => - Verity.bind (Verity.setStorageAddr (Slots.get slots sv) a) (fun _ => - Stmt.meaning rest slots fns env)) - | .local_ e rest => - Verity.bind (Expr.meaning e slots fns env) - (fun x => Stmt.meaning rest slots fns (Env.cons x env)) - | .guard a b msg rest => - Verity.bind (Expr.meaning a slots fns env) (fun x => - Verity.bind (Expr.meaning b slots fns env) (fun y => - Verity.bind (Verity.require (Nat.ble y.val x.val) msg) - (fun _ => Stmt.meaning rest slots fns env))) - | .guardAddrNe a b msg rest => - Verity.bind (Expr.meaning a slots fns env) (fun x => - Verity.bind (Expr.meaning b slots fns env) (fun y => - Verity.bind (Verity.require (decide (x = y)) msg) - (fun _ => Stmt.meaning rest slots fns env))) - | .callStmt fvar args rest => - Verity.bind (Args.eval args slots fns env) (fun vals => - Verity.bind (FnEnv.get fns fvar (envOfVals vals)) (fun _ => - Stmt.meaning rest slots fns env)) - | .seq pre rest => - Verity.bind (Stmt.meaning pre slots fns env) (fun _ => - Stmt.meaning rest slots fns env) - | .block inner post => - Verity.bind (Stmt.meaning inner slots fns env) (fun v => - Verity.bind (Stmt.meaning post slots fns env) (fun _ => Verity.pure v)) -end - -/-- Every imported entry point is non-payable. Internal calls do not re-apply -this guard; they run the callee body under the caller's `msg.value`. -/ -def nonpayable {α : Type} (m : Verity.Contract α) : Verity.Contract α := - Verity.bind Verity.msgValue (fun value => - Verity.bind (Verity.require (Nat.beq value.val 0) "Nonpayable") (fun _ => m)) - -/-- The meaning of a whole function: its body, under the non-payable guard. -/ -def Fn.meaning {L : Layout} {F : Fns} {Γ : Ctx} {r : Ret} - (body : Stmt L F Γ r) (slots : Slots L) (fns : FnEnv F) (env : Env Γ) : - Verity.Contract (Ret.denote r) := - nonpayable (Stmt.meaning body slots fns env) - -/-- Body meaning without the non-payable guard, used for internal functions. -/ -def Fn.bodyMeaning {L : Layout} {F : Fns} {Γ : Ctx} {r : Ret} - (body : Stmt L F Γ r) (slots : Slots L) (fns : FnEnv F) (env : Env Γ) : - Verity.Contract (Ret.denote r) := - Stmt.meaning body slots fns env - --- Every imported entry point is registered as `Fn.meaning` applied to the parsed --- term, so the proofs need these definitions to unfold with the rest of the --- `solidity_import` simp set. -attribute [solidity_import] Fn.meaning Fn.bodyMeaning nonpayable Stmt.meaning assignWith - Expr.meaning Args.eval checked Env.get Slots.get FnEnv.get envOfVals - Ty.denote Ret.denote StorageTy.denote - -end SolidityImporter.Sol diff --git a/Contracts/VaultFromSolidity/Importer/Syntax.lean b/Contracts/VaultFromSolidity/Importer/Syntax.lean deleted file mode 100644 index d0b66e116a..0000000000 --- a/Contracts/VaultFromSolidity/Importer/Syntax.lean +++ /dev/null @@ -1,253 +0,0 @@ -/- - The Solidity subset the Vault importer accepts, as an intrinsically typed - inductive family. - - Ill-typed programs are unrepresentable: reading a `uint256` state variable - yields a `uint`, a mapping index must be an `addr`, a `uint256` local can only - be bound to a `uint`, and `Stmt L F Γ r` only ever produces a value of type `r`. - The parser in `Importer.lean` is the only producer of these terms and - `Semantics.lean` is the only place that gives them meaning. - - `Ctx` is a de Bruijn context: index 0 is the innermost binder. `Layout` is the - imported state-variable order, which the importer keeps in lockstep with the - `Slot` handles it registers. `Fns` is the callee-first function table: - a body may only mention functions already registered, so recursion is - unrepresentable. --/ - -import Lean - -open Lean - -namespace SolidityImporter.Sol - -/-- Types of the accepted subset: `uint256`, `address`, and a two-member -user struct as `uint256 × address`, indexed by the solc struct id so -distinct shapes-alike structs stay distinct in signatures. -/ -inductive Ty where - | uint : Ty - | addr : Ty - | pair : Nat → Ty - deriving DecidableEq, Repr, Inhabited, ToExpr - -/-- What an imported function returns. Empty is `void`; a singleton is the -POC's one named or unnamed return. Multi-value returns wait for a later slice. -/ -abbrev Ret := List Ty - -/-- Shape of an imported state variable. -/ -inductive StorageTy where - | scalar : StorageTy - | mapping : StorageTy - | addr : StorageTy - deriving DecidableEq, Repr, Inhabited, ToExpr - -/-- Parameter/local context, de Bruijn order. -/ -abbrev Ctx := List Ty - -/-- Imported state variables, in importer field order. -/ -abbrev Layout := List StorageTy - -/-- A function signature: parameters in de Bruijn order (last declared -innermost) and the return list. -/ -structure Sig where - params : Ctx - rets : Ret - deriving DecidableEq, Repr, Inhabited, ToExpr - -/-- Already-registered function signatures, callee-first. -/ -abbrev Fns := List Sig - -/-- A parameter or local at a given type in a context. -/ -inductive Var : Ctx → Ty → Type where - | here : Var (t :: Γ) t - | there : Var Γ t → Var (t' :: Γ) t - deriving ToExpr - -/-- A state variable handle of a given storage shape. -/ -inductive SVar : Layout → StorageTy → Type where - | here : SVar (st :: L) st - | there : SVar L st → SVar (st' :: L) st - deriving ToExpr - -/-- A handle of an already-registered function. -/ -inductive FVar : Fns → Sig → Type where - | here : FVar (σ :: F) σ - | there : FVar F σ → FVar (σ' :: F) σ - deriving ToExpr - -/-- Checked arithmetic operators: Solidity's `+` and `-` on `uint256`. -/ -inductive ArithOp where - | add : ArithOp - | sub : ArithOp - deriving DecidableEq, Repr, Inhabited, ToExpr - -/-- Assignment operators: `=`, `+=` and `-=`. -/ -inductive AssignOp where - | set : AssignOp - | add : AssignOp - | sub : AssignOp - deriving DecidableEq, Repr, Inhabited, ToExpr - -/-- The `n`-th variable of a context, if the context is that deep. -/ -def Var.ofIndex : (Γ : Ctx) → (n : Nat) → Option (Σ t, Var Γ t) - | [], _ => none - | _ :: _, 0 => some ⟨_, .here⟩ - | _ :: Γ, n + 1 => (Var.ofIndex Γ n).map fun v => ⟨v.1, .there v.2⟩ - -/-- The `n`-th state variable of a layout, if the layout is that deep. -/ -def SVar.ofIndex : (L : Layout) → (n : Nat) → Option (Σ st, SVar L st) - | [], _ => none - | _ :: _, 0 => some ⟨_, .here⟩ - | _ :: L, n + 1 => (SVar.ofIndex L n).map fun s => ⟨s.1, .there s.2⟩ - -/-- The `n`-th already-registered function, if the table is that long. -/ -def FVar.ofIndex : (F : Fns) → (n : Nat) → Option (Σ σ, FVar F σ) - | [], _ => none - | _ :: _, 0 => some ⟨_, .here⟩ - | _ :: F, n + 1 => (FVar.ofIndex F n).map fun f => ⟨f.1, .there f.2⟩ - -/-- Declaration-order argument list (leftmost first). `Sig.params` is the -de Bruijn reverse of this list. -/ -abbrev DeclParams := List Ty - -mutual -/-- `uint256`/`address` expressions: locals, `msg.sender`, literals, state -reads, checked `+`/`-`, and internal calls that return one value. -/ -inductive Expr (L : Layout) (F : Fns) (Γ : Ctx) : Ty → Type where - /-- A parameter or local. -/ - | var : Var Γ t → Expr L F Γ t - /-- `msg.sender`. -/ - | sender : Expr L F Γ .addr - /-- The zero address, used for named-return defaults. -/ - | zero : Expr L F Γ .addr - /-- A numeric literal; the parser enforces `n < 2^256`. -/ - | lit : Nat → Expr L F Γ .uint - /-- `uint256` state variable read. -/ - | load : SVar L .scalar → Expr L F Γ .uint - /-- `address` state variable read. -/ - | loadAddr : SVar L .addr → Expr L F Γ .addr - /-- `mapping(address => uint256)` read. -/ - | index : SVar L .mapping → Expr L F Γ .addr → Expr L F Γ .uint - /-- Checked `+`/`-` on `uint256`, reverting with `Panic(0x11)`. -/ - | arith : ArithOp → Expr L F Γ .uint → Expr L F Γ .uint → Expr L F Γ .uint - /-- Memory struct `{amount, who}` of solc struct `id`. Evaluates amount then who - (positional `Acc(a, b)` and named `Acc({amount, who})`). -/ - | pair : {id : Nat} → Expr L F Γ .uint → Expr L F Γ .addr → Expr L F Γ (.pair id) - /-- Same packing as `pair`, but evaluates who then amount. Named - `Acc({who, amount})` uses this so source-order eval matches pinned solc 0.8.33 - before the result is packed as `uint256 × address`. -/ - | pairRev : {id : Nat} → Expr L F Γ .addr → Expr L F Γ .uint → Expr L F Γ (.pair id) - /-- First struct member (`amount`). -/ - | fst : {id : Nat} → Expr L F Γ (.pair id) → Expr L F Γ .uint - /-- Second struct member (`who`). -/ - | snd : {id : Nat} → Expr L F Γ (.pair id) → Expr L F Γ .addr - /-- Internal call of an already-registered `view`/`pure` function that - returns one value. Arguments are in declaration order and evaluate left - to right. Effectful internals are `Stmt.callStmt` only: pinned solc 0.8.x - legacy codegen evaluates those calls before the other operand / old-read. -/ - | call : {ts : List Ty} → - FVar F ⟨ts.reverse, [t]⟩ → Args L F Γ ts → Expr L F Γ t - deriving ToExpr - -/-- Declaration-order arguments of an internal call. -/ -inductive Args (L : Layout) (F : Fns) (Γ : Ctx) : List Ty → Type where - | nil : Args L F Γ [] - | cons : Expr L F Γ t → Args L F Γ ts → Args L F Γ (t :: ts) - deriving ToExpr -end - -/-- Insert `t'` at de Bruijn depth `n` (0 = innermost). -/ -def insertAt : Nat → Ty → Ctx → Ctx - | 0, t', Γ => t' :: Γ - | n + 1, t', t :: Γ => t :: insertAt n t' Γ - | _ + 1, t', [] => [t'] - -/-- Shift a variable across a binder inserted at depth `n`. -/ -def Var.shift (t' : Ty) : (n : Nat) → {Γ : Ctx} → Var Γ t → Var (insertAt n t' Γ) t - | 0, _, v => .there v - | _ + 1, _, .here => .here - | n + 1, _, .there v => .there (Var.shift t' n v) - -mutual -/-- Shift an expression across a binder inserted at depth `n`. -/ -def Expr.shift (t' : Ty) {L : Layout} {F : Fns} (n : Nat) {Γ : Ctx} {t : Ty} : - Expr L F Γ t → Expr L F (insertAt n t' Γ) t - | .var v => .var (v.shift t' n) - | .sender => .sender - | .zero => .zero - | .lit k => .lit k - | .load s => .load s - | .loadAddr s => .loadAddr s - | .index s key => .index s (key.shift t' n) - | .arith op a b => .arith op (a.shift t' n) (b.shift t' n) - | .pair a b => .pair (a.shift t' n) (b.shift t' n) - | .pairRev a b => .pairRev (a.shift t' n) (b.shift t' n) - | .fst e => .fst (e.shift t' n) - | .snd e => .snd (e.shift t' n) - | .call fv args => .call fv (args.shift t' n) - -/-- Shift call arguments across a binder inserted at depth `n`. -/ -def Args.shift (t' : Ty) {L : Layout} {F : Fns} (n : Nat) {Γ : Ctx} {ts : List Ty} : - Args L F Γ ts → Args L F (insertAt n t' Γ) ts - | .nil => .nil - | .cons e rest => .cons (e.shift t' n) (rest.shift t' n) -end - -/-- Assignment targets: a `uint256` state variable or one mapping entry. -Address writes use `Stmt.assignAddr`. -/ -inductive LVal (L : Layout) (F : Fns) (Γ : Ctx) : Type where - | scalar : SVar L .scalar → LVal L F Γ - | mapping : SVar L .mapping → Expr L F Γ .addr → LVal L F Γ - deriving ToExpr - -/-- Statements of the accepted subset, in source order, ending in either -`done` (no return value) or `ret` of a single value. -/ -inductive Stmt (L : Layout) (F : Fns) : Ctx → Ret → Type where - /-- The end of a body that returns nothing. -/ - | done : Stmt L F Γ [] - /-- A terminal `return e;`. -/ - | ret : Expr L F Γ t → Stmt L F Γ [t] - /-- `lval op= rhs;` followed by the rest of the body. `+=`/`-=` are uint-only. -/ - | assign : LVal L F Γ → AssignOp → Expr L F Γ .uint → Stmt L F Γ r → Stmt L F Γ r - /-- `addr = rhs;` followed by the rest of the body. -/ - | assignAddr : SVar L .addr → Expr L F Γ .addr → Stmt L F Γ r → Stmt L F Γ r - /-- `uint256 x = e;` (or a parameter), followed by the rest of the body. -/ - | local_ : Expr L F Γ t → Stmt L F (t :: Γ) r → Stmt L F Γ r - /-- `if (a < b) revert E();` with `E()` stored verbatim, followed by the rest - of the body. -/ - | guard : Expr L F Γ .uint → Expr L F Γ .uint → String → Stmt L F Γ r → Stmt L F Γ r - /-- `if (a != b) revert E();` on addresses. -/ - | guardAddrNe : Expr L F Γ .addr → Expr L F Γ .addr → String → Stmt L F Γ r → Stmt L F Γ r - /-- Internal call of an already-registered void function, then the rest. -/ - | callStmt : {ts : List Ty} → - FVar F ⟨ts.reverse, []⟩ → Args L F Γ ts → Stmt L F Γ r → Stmt L F Γ r - /-- Run a void prefix, then the rest. Used for modifier preludes. -/ - | seq : Stmt L F Γ [] → Stmt L F Γ r → Stmt L F Γ r - /-- Run `inner`; its `ret`/`done` exits this block only. Then run void `post` - and yield `inner`'s value. Modifier postludes use this so they run after - an early return. -/ - | block : Stmt L F Γ r → Stmt L F Γ [] → Stmt L F Γ r - deriving ToExpr - -/-- Shift an assignment target across a binder inserted at depth `n`. -/ -def LVal.shift (t' : Ty) {L : Layout} {F : Fns} (n : Nat) {Γ : Ctx} : - LVal L F Γ → LVal L F (insertAt n t' Γ) - | .scalar s => .scalar s - | .mapping s key => .mapping s (key.shift t' n) - -/-- Shift a statement across a binder inserted at depth `n`. -/ -def Stmt.shift (t' : Ty) {L : Layout} {F : Fns} (n : Nat) {Γ : Ctx} {r : Ret} : - Stmt L F Γ r → Stmt L F (insertAt n t' Γ) r - | .done => .done - | .ret e => .ret (e.shift t' n) - | .assign lv op rhs rest => .assign (lv.shift t' n) op (rhs.shift t' n) (rest.shift t' n) - | .assignAddr s rhs rest => .assignAddr s (rhs.shift t' n) (rest.shift t' n) - | .local_ e rest => .local_ (e.shift t' n) (rest.shift t' (n + 1)) - | .guard a b msg rest => .guard (a.shift t' n) (b.shift t' n) msg (rest.shift t' n) - | .guardAddrNe a b msg rest => - .guardAddrNe (a.shift t' n) (b.shift t' n) msg (rest.shift t' n) - | .callStmt fv args rest => .callStmt fv (args.shift t' n) (rest.shift t' n) - | .seq pre rest => .seq (pre.shift t' n) (rest.shift t' n) - | .block inner post => .block (inner.shift t' n) (post.shift t' n) - -end SolidityImporter.Sol diff --git a/Contracts/VaultFromSolidity/Importer/scripts/solidity_importer_test.py b/Contracts/VaultFromSolidity/Importer/scripts/solidity_importer_test.py deleted file mode 100644 index d0b7395fad..0000000000 --- a/Contracts/VaultFromSolidity/Importer/scripts/solidity_importer_test.py +++ /dev/null @@ -1,591 +0,0 @@ -#!/usr/bin/env python3 -"""Acceptance checks for the Lean-only Solidity frontend. - -Python only orchestrates disposable builds and mutations. The production import -path is `Contracts/VaultFromSolidity/Importer/Importer.lean` -> pinned solc -> checked Lean declarations. -""" - -import hashlib -import os -from pathlib import Path -import re -import shutil -import subprocess -import sys -import tempfile - -ROOT = Path(__file__).resolve().parents[4] - - -def lake_binary() -> str: - found = shutil.which("lake") - if found: - return found - version = (ROOT / "lean-toolchain").read_text().strip().split(":")[-1] - candidates = [ - Path.home() / ".elan/toolchains" / f"leanprover--lean4---{version}" / "bin/lake", - Path("/home/claudine/.hermes/profiles/claudine/home/.elan/toolchains") - / f"leanprover--lean4---{version}" / "bin/lake", - ] - for candidate in candidates: - if candidate.is_file(): - return str(candidate) - raise RuntimeError("lake executable not found") - - -LAKE = lake_binary() -ENV = dict(os.environ) -SOLC_PIN = ( - "8324280591ce398d7e2722846bc10ecf1779b13a328ef97b687c92cd9c70801a" - if sys.platform == "darwin" - else "1274e5c4621ae478090c5a1f48466fd3c5f658ed9e14b15a0b213dc806215468" -) - - -def check(ok: bool, message: str) -> None: - if not ok: - raise AssertionError(message) - print("PASS " + message, flush=True) - - -def run(root: Path, args: list[str], success: bool = True, contains: str | None = None) -> str: - process = subprocess.run( - args, cwd=root, env=ENV, text=True, capture_output=True, timeout=300 - ) - output = process.stdout + process.stderr - if (process.returncode == 0) != success or (contains and contains not in output) or "PANIC" in output: - raise AssertionError(f"{args}: exit {process.returncode}\n{output}") - return output - - -def main() -> None: - importer_path = ROOT / "Contracts/VaultFromSolidity/Importer/Importer.lean" - importer_text = importer_path.read_text() - syntax_path = ROOT / "Contracts/VaultFromSolidity/Importer/Syntax.lean" - semantics_path = ROOT / "Contracts/VaultFromSolidity/Importer/Semantics.lean" - syntax_text = syntax_path.read_text() - semantics_text = semantics_path.read_text() - python_frontend = ROOT / "Contracts/VaultFromSolidity/Importer/scripts/solidity_importer.py" - check(not python_frontend.exists(), "no Python importer/frontend exists") - check("--standard-json" in importer_text and "--no-import-callback" in importer_text, - "Lean importer invokes pinned solc standard JSON with import callback disabled") - check("officialSolcSha256s" in importer_text and "compiler checksum mismatch" in importer_text, - "Lean importer enforces official compiler checksum allowlist and version pin") - check("0.8.33+commit.64118f21" in importer_text and "solcVersionPin" in importer_text, - "Lean importer pins the official solc release identity") - check(SOLC_PIN in importer_text and - "1274e5c4621ae478090c5a1f48466fd3c5f658ed9e14b15a0b213dc806215468" in importer_text, - "Lean importer allowlists official linux-amd64 and macosx-amd64 checksums") - check('cmd := "/usr/bin/sha256sum"' in importer_text and 'cmd := "sha256sum"' not in importer_text, - "Linux compiler checksum utility uses a fixed path, not PATH lookup") - check('cmd := "/usr/bin/shasum"' in importer_text and 'cmd := "shasum"' not in importer_text, - "macOS compiler checksum utility uses a fixed path, not PATH lookup") - check("parseExpr" in importer_text and "parseStmts" in importer_text and - "Expr.meaning" in semantics_text and "Stmt.meaning" in semantics_text, - "Solidity constructs have explicit Lean parser and semantics functions") - check("inductive Expr" in syntax_text and "inductive Stmt" in syntax_text and - "inductive LVal" in syntax_text, - "accepted Solidity subset is the closed inductive in Syntax.lean") - check(all(tag not in importer_text for tag in ('[\"read\"', '[\"write\"', '[\"guard\"')), - "no custom serialized JSON IR tags") - - with tempfile.TemporaryDirectory(prefix="verity-vault-check-", dir=ROOT.parent) as directory: - root = Path(directory) - for name in ("Verity", "Compiler", "Contracts", "scripts"): - shutil.copytree(ROOT / name, root / name) - for name in ("lakefile.lean", "lake-manifest.json", "lean-toolchain"): - shutil.copy2(ROOT / name, root / name) - shutil.copytree(ROOT / ".lake/build", root / ".lake/build") - (root / ".lake/solidity-import").mkdir(parents=True) - shutil.copy2(ROOT / ".lake/solidity-import/solc", root / ".lake/solidity-import/solc") - (root / ".lake/packages").symlink_to(ROOT / ".lake/packages", target_is_directory=True) - - source = root / "Contracts/VaultFromSolidity/Vault.sol" - original = source.read_bytes() - source_stamp = source.stat() - importer = root / "Contracts/VaultFromSolidity/Importer/Importer.lean" - importer_original = importer.read_bytes() - importer_stamp = importer.stat() - compiler = root / ".lake/solidity-import/solc" - compiler_original = compiler.read_bytes() - lean_sources = set(root.rglob("*.lean")) - - def edit_source(data: bytes) -> None: - source.write_bytes(data) - os.utime(source, ns=(source_stamp.st_atime_ns, source_stamp.st_mtime_ns)) - - def build(success: bool = True, contains: str | None = None) -> str: - return run(root, [LAKE, "build", "VaultFromSolidity"], success, contains) - - def artifacts() -> dict[str, tuple[int, str]]: - paths = list((root / ".lake/build/lib/lean/Contracts/VaultFromSolidity").rglob("*.olean")) - return { - str(path.relative_to(root)): ( - path.stat().st_mtime_ns, - hashlib.sha256(path.read_bytes()).hexdigest(), - ) - for path in paths - } - - def source_digest() -> str: - digest_probe = root / ".lake/solidity-import/SourceDigestProbe.lean" - try: - digest_probe.write_text( - "import Contracts.VaultFromSolidity.VaultFromSolidity\n" - "#eval Contracts.VaultFromSolidity.sourceDigest\n" - ) - output = run(root, [LAKE, "env", "lean", str(digest_probe)]) - match = re.search(r'"([0-9a-f]{64})"', output) - if match is None: - raise AssertionError("sourceDigest is not an auditable SHA-256 value") - check(True, "sourceDigest is an auditable SHA-256 value") - return match.group(1) - finally: - digest_probe.unlink(missing_ok=True) - - build() - check(True, "baseline lake build VaultFromSolidity") - - ux_probe = root / "Contracts/VaultFromSolidity/FrontendProbe.lean" - try: - ux_probe.write_text('''import Contracts.VaultFromSolidity.Importer.Importer -solidity_contract Vault from "Vault.sol" -#print Vault.deposit -''') - ux_output = run(root, [LAKE, "env", "lean", str(ux_probe)]) - check("def Vault.deposit" in ux_output and "Sol.Fn.meaning" in ux_output and - "Sol.Stmt.assign" in ux_output, - "documented solidity_contract Vault UX exposes #print Vault.deposit as its parsed AST") - finally: - ux_probe.unlink(missing_ok=True) - - proof = root / "Contracts/VaultFromSolidity/Proofs/ExecutionProof.lean" - proof_text = proof.read_text() - theorem_names = re.findall(r"^theorem\s+(\w+)", proof_text, re.M) - audit_file = root / ".lake/solidity-import/AxiomAudit.lean" - try: - audit_file.write_text( - "import Contracts.VaultFromSolidity.Proofs.ExecutionProof\n" - + "\n".join( - "#print axioms Contracts.VaultFromSolidity.Proofs.ExecutionProof." + name - for name in theorem_names - ) - + "\n" - ) - audit = run(root, [LAKE, "env", "lean", str(audit_file)]) - finally: - audit_file.unlink(missing_ok=True) - entries = re.findall( - r"'Contracts.VaultFromSolidity.Proofs.ExecutionProof.(\w+)' depends on axioms: \[([^\]]*)\]", audit - ) - check(set(theorem_names) == {name for name, _ in entries}, - "every theorem appears in actual #print axioms output") - axioms = {a.strip() for _, values in entries for a in values.split(",") if a.strip()} - check(axioms <= {"propext", "Quot.sound"}, - "no project axioms or sorryAx: " + ", ".join(sorted(axioms))) - - probe = root / ".lake/solidity-import/RegistrationProbe.lean" - try: - probe.write_text('''import Contracts.VaultFromSolidity.VaultFromSolidity -import Contracts.VaultFromSolidity.Importer.Semantics -open Lean Elab Command -#print Contracts.VaultFromSolidity.deposit -#print SolidityImporter.Sol.Fn.meaning -#print SolidityImporter.Sol.assignWith -#print SolidityImporter.Sol.checked -#check fun (v : Contracts.VaultFromSolidity.Storage) => v.totalAssets -#print Contracts.VaultFromSolidity.view -#print Contracts.VaultFromSolidity.step -/-- Transitive used-constant closure. A pattern-matching definition over an indexed -family is compiled through a `.brecOn` recursor plus a `._f` companion, so the -Verity primitives a construct unfolds to live in those companions rather than in -the top-level constant's own body. -/ -partial def semanticsClosure (env : Environment) (roots : Array Name) : Array Name := - let rec go (seen todo : Array Name) : Array Name := - if todo.isEmpty then seen - else - let cur := todo.back! - let todo := todo.pop - if seen.contains cur then go seen todo - else - let seen := seen.push cur - let deps := match env.find? cur with - | some (.defnInfo i) => i.value.getUsedConstants - | some (.thmInfo i) => i.value.getUsedConstants - | _ => #[] - go seen (todo ++ deps) - go #[] roots -run_cmd do - for suffix in ["totalAssetsSlot", "totalSupplySlot", "shareBalancesSlot", - "deposit", "withdraw", "balanceOf", "totalAssets", "totalSupply", - "shareBalances", "sourceDigest", "Storage.totalAssets", - "Storage.totalSupply", "Storage.shareBalances", "view", "step"] do - let name := `Contracts.VaultFromSolidity ++ suffix.toName - let some (.defnInfo info) := (← getEnv).find? name - | throwError "not a transparent definition: {name}" - unless info.safety == .safe && !info.value.hasMVar && !info.value.hasFVar do - throwError "unsafe or unclosed definition: {name}" - for dep in info.value.getUsedConstants do - if dep.toString.startsWith "Contracts." && - !dep.toString.startsWith "Contracts.VaultFromSolidity." then - throwError "imported declaration depends on handwritten contract: {dep}" - let some (.inductInfo _) := (← getEnv).find? `Contracts.VaultFromSolidity.Storage - | throwError "Storage is not an inductive structure" - let some (.ctorInfo _) := (← getEnv).find? `Contracts.VaultFromSolidity.Storage.mk - | throwError "Storage.mk is not a constructor" - let some (.defnInfo deposit) := (← getEnv).find? `Contracts.VaultFromSolidity.deposit - | throwError "missing imported deposit" - -- The imported body is the parsed Solidity AST under `Sol.Fn.meaning`, so the - -- constructors it uses are the source's constructs. - for dep in [``SolidityImporter.Sol.Fn.meaning, ``SolidityImporter.Sol.LVal.mapping, - ``SolidityImporter.Sol.LVal.scalar, ``SolidityImporter.Sol.AssignOp.add] do - unless deposit.value.getUsedConstants.contains dep do - throwError "imported deposit does not go through the parsed AST: {dep}" - -- ... and that meaning still bottoms out in the Verity primitives, reached - -- through the semantics definitions (including their generated companions). - let semantics := semanticsClosure (← getEnv) #[``SolidityImporter.Sol.Fn.meaning, - ``SolidityImporter.Sol.Stmt.meaning, ``SolidityImporter.Sol.assignWith, - ``SolidityImporter.Sol.checked, ``SolidityImporter.Sol.Expr.meaning] - for dep in [``Verity.setMapping, ``Verity.setStorage, ``Verity.Stdlib.Math.safeAdd] do - unless semantics.contains dep do - throwError "semantics does not bottom out in Verity primitive: {dep}" - logInfo "CHECKED_TRANSPARENT_DECLARATIONS" -solidity_contract Existing from "../../Contracts/VaultFromSolidity/Vault.sol" -run_cmd do - let original ← getEnv - let mut rejected := false - try - SolidityImporter.elabSolidityContract - (← `(command| solidity_contract $(mkIdent `Existing):ident from "../../Contracts/VaultFromSolidity/Vault.sol")) - catch _ => rejected := true - unless rejected do throwError "duplicate alias accepted" - let some (.defnInfo before) := original.find? `Existing.deposit - | throwError "missing initial declaration" - let some (.defnInfo after) := (← getEnv).find? `Existing.deposit - | throwError "lost initial declaration" - unless before.value == after.value && before.type == after.type do - throwError "duplicate alias changed prior declaration" - logInfo "DUPLICATE_ALIAS_REJECTED" -''') - output = run(root, [LAKE, "env", "lean", str(probe)]) - check("CHECKED_TRANSPARENT_DECLARATIONS" in output and - "DUPLICATE_ALIAS_REJECTED" in output, - "safe transparent readable definitions and collision rollback") - check("SolidityImporter.Sol.Fn.meaning" in output and - "SolidityImporter.Sol.Stmt.assign" in output and - "SolidityImporter.Sol.LVal.mapping" in output, - "#print deposit exposes the parsed Solidity AST under its meaning") - check("Verity.bind" in output and "SolidityImporter.Sol.checked" in output and - "safeAdd" in output, - "#print of the semantics exposes the Verity primitives it bottoms out in") - check("fun v => v.totalAssets : Contracts.VaultFromSolidity.Storage → Verity.Uint256" in output, - "named storage view supports v.totalAssets dot notation") - check("def Contracts.VaultFromSolidity.view" in output and - "shareBalancesSlot.slot" in output and "readMap" in output and - "readSlot" in output, - "#print view constructs Storage from named slot handles") - check("def Contracts.VaultFromSolidity.step" in output and - "deposit" in output and "totalSupply" in output and - "∃" in output, - "#print step is the entry-point relation") - finally: - probe.unlink(missing_ok=True) - - before = artifacts() - build() - check(before == artifacts(), "unchanged build reuses Vault artifacts") - - theorem_starts = [ - (match.group(1), line_no) - for line_no, line in enumerate(proof_text.splitlines(), 1) - if (match := re.match(r"theorem\s+([A-Za-z0-9_']+)", line.strip())) - ] - theorem_ranges = { - name: (start, theorem_starts[index + 1][1] - 1 if index + 1 < len(theorem_starts) - else len(proof_text.splitlines())) - for index, (name, start) in enumerate(theorem_starts) - } - - edit_source(original.replace(b"assets", b"depositAmount")) - build() - check(True, "declaration-ID based parameter rename preserves proofs") - edit_source(original) - build() - - def slot_numbers() -> list[str]: - slot_probe = root / ".lake/solidity-import/SlotProbe.lean" - try: - slot_probe.write_text( - "import Contracts.VaultFromSolidity.VaultFromSolidity\n" - "#eval [Contracts.VaultFromSolidity.totalAssetsSlot.slot," - " Contracts.VaultFromSolidity.totalSupplySlot.slot," - " Contracts.VaultFromSolidity.shareBalancesSlot.slot]\n" - ) - return re.findall(r"\[\d+, \d+, \d+\]", run(root, [LAKE, "env", "lean", str(slot_probe)])) - finally: - slot_probe.unlink(missing_ok=True) - - declarations = b" uint256 public totalAssets;\n uint256 public totalSupply;\n" - check(original.count(declarations) == 1 and slot_numbers() == ["[0, 1, 2]"], - "baseline storage layout puts totalAssets, totalSupply, shareBalances at slots 0, 1, 2") - edit_source(original.replace( - declarations, b" uint256 public totalSupply;\n uint256 public totalAssets;\n")) - build() - check(slot_numbers() == ["[1, 0, 2]"], - "reordering state variables binds the spec names to the new slots and proofs still pass") - edit_source(original) - build() - - edit_source(original.replace(b"totalAssets", b"assetsTotal")) - output = build(False, "Invalid field `totalAssets`") - check(re.search(r"Contracts/VaultFromSolidity/Spec\.lean:\d+:\d+:", output) is not None, - "renaming a state variable makes the named spec fail to elaborate") - edit_source(original) - build() - - def broken_theorems(output: str) -> set[str]: - error_lines = [int(value) for value in re.findall( - r"Contracts/VaultFromSolidity/Proofs/ExecutionProof\.lean:(\d+):", output)] - return {name for name, (start, end) in theorem_ranges.items() - if any(start <= line <= end for line in error_lines)} - - # A behaviour change in Vault.sol must break both the success theorem - # and the human-facing `*_meets_spec` theorem. Lean adds a failed - # theorem with `sorry`, so a spec theorem derived from the success - # lemma would keep elaborating; requiring an error inside its own range - # shows the spec layer is proved against the imported definitions itself. - mint = b" function mint(uint256 a) external { totalSupply += a; }\n" - old_balance = b" function balanceOf(address account) external view returns (uint256) {" - for name, theorems, old, new in ( - ("deposit behavior", ("deposit_success_spec", "deposit_meets_spec"), - b"totalSupply += assets;", b"totalSupply = assets;"), - ("getter behavior", ("balance_success_spec", "balance_meets_spec"), - b"return shareBalances[account];", b"return totalAssets;"), - ("new entry point", ("solvent_invariant",), old_balance, mint + old_balance), - ): - check(original.count(old) == 1, name + " mutation has one source target") - before = artifacts() - edit_source(original.replace(old, new)) - output = build(False, "Contracts.VaultFromSolidity.Proofs.ExecutionProof") - broken = broken_theorems(output) - for theorem in theorems: - check(theorem in broken, name + f" mutation breaks {theorem}") - check(before != artifacts(), name + " preserved-mtime edit refreshes artifacts") - edit_source(original) - build() - - - # The spec layer is not vacuous: a wrong promise in Spec.lean is unprovable - # against the unchanged Solidity, and the failure lands in both the - # corresponding success theorem and the `*_meets_spec` theorem. - spec = root / "Contracts/VaultFromSolidity/Spec.lean" - spec_original = spec.read_bytes() - for name, theorems, old, new in ( - ("deposit spec without the supply increment", - ("deposit_success_spec", "deposit_meets_spec"), - b"post.totalSupply = pre.totalSupply + amount", b"post.totalSupply = pre.totalSupply"), - ("withdraw spec without the supply decrement", - ("withdraw_success_spec", "withdraw_meets_spec"), - b"post.totalSupply = pre.totalSupply - amount", b"post.totalSupply = pre.totalSupply"), - ("getter spec returning the wrong variable", - ("balance_success_spec", "balance_meets_spec"), - b"result = v.shareBalances account", b"result = v.totalSupply"), - ): - check(spec_original.count(old) == 1, name + " mutation has one spec target") - spec.write_bytes(spec_original.replace(old, new)) - try: - output = build(False, "Contracts.VaultFromSolidity.Proofs.ExecutionProof") - finally: - spec.write_bytes(spec_original) - broken = broken_theorems(output) - for theorem in theorems: - check(theorem in broken, name + f" is unprovable: breaks {theorem}") - build() - - for name, old, new, diagnostic in ( - ("contract layout at", b"contract Vault {", b"contract Vault layout at 100 {", "layout at"), - ("initializer", b"uint256 public totalAssets;", b"uint256 public totalAssets = 1;", "initializer"), - ("unchecked block", b"totalAssets += assets;", b"unchecked { totalAssets += assets; }", "UncheckedBlock"), - ("loop", b"totalAssets += assets;", b"while (assets < totalAssets) { totalAssets += assets; }", "WhileStatement"), - ("second contract", b"contract Vault {", b"contract Other {}\ncontract Vault {", "exactly one"), - ("multiplication", b"totalAssets += assets;", b"totalAssets = totalAssets * assets;", "unsupported binary"), - ("reserved Storage name", b"uint256 public totalSupply;", - b"uint256 public totalSupply;\n uint256 public Storage;", "unsupported/reserved name"), - ("reserved step name", b"uint256 public totalSupply;", - b"uint256 public totalSupply;\n uint256 public step;", "unsupported/reserved name"), - ): - edit_source(original.replace(old, new)) - output = build(False, diagnostic) - check(re.search(r"Contracts/VaultFromSolidity/Vault.sol:\d+:\d+:", output) is not None, - name + " rejected with source position") - edit_source(original) - build() - - with tempfile.TemporaryDirectory(prefix="verity-vault-outside-", dir=ROOT.parent) as outside: - escaped = Path(outside) / "Vault.sol" - escaped.write_bytes(original) - source.unlink() - source.symlink_to(escaped) - try: - probe.write_text('''import Contracts.VaultFromSolidity.Importer.Importer -solidity_contract Escaped from "../../Contracts/VaultFromSolidity/Vault.sol" -''') - run(root, [LAKE, "env", "lean", str(probe)], False, "source outside package") - check(True, "registered-source symlink escape rejected") - finally: - probe.unlink(missing_ok=True) - source.unlink() - edit_source(original) - build() - - before = artifacts() - digest_before = source_digest() - importer.write_bytes(importer_original + b"\n-- acceptance translation identity probe\n") - os.utime(importer, ns=(importer_stamp.st_atime_ns, importer_stamp.st_mtime_ns)) - build() - check(before != artifacts(), "Lean importer content change invalidates Vault artifacts") - check(digest_before != source_digest(), "Lean importer content changes sourceDigest") - importer.write_bytes(importer_original) - build() - - # The semantics is part of the trusted path, so its content must move the - # digest exactly like the parser does. - semantics = root / "Contracts/VaultFromSolidity/Importer/Semantics.lean" - semantics_original = semantics.read_bytes() - semantics_stamp = semantics.stat() - before = artifacts() - digest_before = source_digest() - semantics.write_bytes(semantics_original + b"\n-- acceptance semantics identity probe\n") - os.utime(semantics, ns=(semantics_stamp.st_atime_ns, semantics_stamp.st_mtime_ns)) - build() - check(before != artifacts(), "Lean semantics content change invalidates Vault artifacts") - check(digest_before != source_digest(), "Lean semantics content changes sourceDigest") - semantics.write_bytes(semantics_original) - build() - - policy = root / "lakefile.lean" - policy_original = policy.read_bytes() - before = artifacts() - policy.write_bytes(policy_original + b"\n-- acceptance build-policy probe\n") - build() - check(before != artifacts(), "build-policy content change invalidates Vault artifacts") - policy.write_bytes(policy_original) - build() - - compiler_stamp = compiler.stat() - compiler.write_bytes(compiler_original + b"\nacceptance-check\n") - os.utime(compiler, ns=(compiler_stamp.st_atime_ns, compiler_stamp.st_mtime_ns)) - build(False, "compiler checksum mismatch") - check(True, "compiler content mutation fails closed") - compiler.write_bytes(compiler_original) - build() - - # Synthetic compiler-output probes test the JSON boundary itself. The - # temporary wrapper is checksummed and accepted only in this disposable - # package; production still executes the pinned binary directly. - real_compiler = compiler.with_name("solc-real") - pin = SOLC_PIN.encode() - for mode, diagnostic in ( - ("ast", "unexpected AST fields"), - ("metadata", "unexpected AST fields"), - ("typed", "invalid AST metadata field Assignment.isLValue"), - ("missing", "missing AST fields"), - ("span", "source span outside registered source"), - ("layout", "missing/packed layout"), - ): - try: - real_compiler.write_bytes(compiler_original) - real_compiler.chmod(0o755) - compiler.write_text(f'''#!/usr/bin/env python3 -import json, pathlib, subprocess, sys -real = pathlib.Path(__file__).with_name("solc-real") -p = subprocess.run([str(real), *sys.argv[1:]], input=sys.stdin.buffer.read(), capture_output=True) -if "--standard-json" not in sys.argv: - sys.stdout.buffer.write(p.stdout); sys.stderr.buffer.write(p.stderr); raise SystemExit(p.returncode) -o = json.loads(p.stdout) -if {mode!r} in ("ast", "metadata", "typed", "missing", "span"): - def mutate(x): - if isinstance(x, dict): - if {mode!r} == "ast" and x.get("nodeType") == "Assignment": - x["unknownExecutableField"] = True; return True - if {mode!r} == "metadata" and x.get("nodeType") == "StructuredDocumentation": - x["unexpectedExecutable"] = {{"nodeType": "UncheckedBlock", "id": 999999, "src": "0:0:0"}} - return True - if {mode!r} == "typed" and x.get("nodeType") == "Assignment": - x["isLValue"] = {{"nodeType": "UncheckedBlock", "id": 999999, "src": "0:0:0"}} - return True - if {mode!r} == "missing" and x.get("nodeType") == "Assignment": - del x["isPure"] - return True - if {mode!r} == "span" and x.get("nodeType") == "Assignment": - start, size, _ = x["src"].split(":") - x["src"] = f"{{start}}:{{size}}:999" - return True - return any(mutate(v) for v in x.values()) - if isinstance(x, list): return any(mutate(v) for v in x) - return False - assert mutate(o) -else: - o["contracts"]["Contracts/VaultFromSolidity/Vault.sol"]["Vault"]["storageLayout"]["storage"][0]["offset"] = 1 -sys.stdout.write(json.dumps(o)) -''') - compiler.chmod(0o755) - wrapper_hash = hashlib.sha256(compiler.read_bytes()).hexdigest().encode() - check(pin in importer_original, "compiler pin occurs in Lean importer") - importer.write_bytes(importer_original.replace(pin, wrapper_hash)) - run(root, [LAKE, "build", "VaultSolidityImporter"]) - build(False, diagnostic) - check(True, f"synthetic {mode} compiler output fails closed") - finally: - real_compiler.unlink(missing_ok=True) - compiler.write_bytes(compiler_original) - compiler.chmod(0o755) - importer.write_bytes(importer_original) - build() - - # Corrupt a late declaration's type. Synchronous checking must reject it - # before any declaration from the failed namespace escapes the transaction. - try: - importer.write_bytes(importer_original.replace( - b" addDecl (.defnDecl { name, levelParams := [], type, value, hints := .regular 0, safety := .safe })", - b' let type := if name.toString.endsWith ".deposit" then mkConst ``Nat else type\n' - b" addDecl (.defnDecl { name, levelParams := [], type, value, hints := .regular 0, safety := .safe })", - )) - run(root, [LAKE, "build", "VaultSolidityImporter"]) - probe.write_text('''import Contracts.VaultFromSolidity.Importer.Importer -open Lean Elab Command -set_option Elab.async true -run_cmd do - let mut rejected := false - try - SolidityImporter.elabSolidityContract - (← `(command| solidity_contract $(mkIdent `Broken):ident from "../../Contracts/VaultFromSolidity/Vault.sol")) - catch e => - rejected := true - logInfo m!"EXPECTED_KERNEL_ERROR {e.toMessageData}" - unless rejected do throwError "malformed declaration accepted" - for suffix in ["totalAssetsSlot", "totalSupplySlot", "shareBalancesSlot", - "totalAssets", "totalSupply", "shareBalances", "deposit", "sourceDigest", - "Storage", "Storage.mk", "Storage.totalAssets", "Storage.totalSupply", - "Storage.shareBalances", "view", "step"] do - if (← getEnv).contains (`Broken ++ suffix.toName) then - throwError "partial declaration escaped rollback: {suffix}" - logInfo "KERNEL_REJECTION_ROLLED_BACK" -''') - output = run(root, [LAKE, "env", "lean", str(probe)]) - check("KERNEL_REJECTION_ROLLED_BACK" in output and "(kernel)" in output, - "malformed late declaration rejected synchronously with full rollback") - finally: - probe.unlink(missing_ok=True) - importer.write_bytes(importer_original) - build() - - check(set(root.rglob("*.lean")) == lean_sources, - "no generated model .lean files") - check(source.read_bytes() == original and importer.read_bytes() == importer_original, - "temporary mutations restored; final baseline passes") - print(f"PASS all Lean-only Vault acceptance checks ({len(theorem_names)} audited theorems)", flush=True) - - -if __name__ == "__main__": - main() diff --git a/Contracts/VaultFromSolidity/Proofs/ExecutionProof.lean b/Contracts/VaultFromSolidity/Proofs/ExecutionProof.lean deleted file mode 100644 index 7ed8a8b933..0000000000 --- a/Contracts/VaultFromSolidity/Proofs/ExecutionProof.lean +++ /dev/null @@ -1,153 +0,0 @@ -import Contracts.VaultFromSolidity.Spec -import Verity.Proofs.Stdlib.SolidityImport - -/-! -# Proofs about the Vault imported from Solidity - -Three layers, deliberately kept small: - -1. `*_success_spec` -- each successful entry point produces a post-state (and, - for `balanceOf`, a return value) that satisfies the named-storage spec. - These unfold the definitions `Importer.lean` registered from `Vault.sol`, so - they fail if the Solidity source changes behaviour. Reverting calls are - excluded by the success hypothesis. -2. `*_meets_spec` -- the named-storage promise from `Spec` under its - precondition. Each states that the call succeeds and that the successful - post-state (or, for `balanceOf`, the returned value) satisfies the spec. -3. `solvent_invariant` -- the contract-level result: `solvent` is preserved by - the importer-generated `step` relation covering every public entry point. - A reverting call leaves the state untouched (`Contract.run` rolls back; - theorem `run_snd_cases`), so solvency can only ever be lost on a successful - call, which is what the success theorems and this invariant rule out. - -Slots are only ever referenced through the imported `Slot` handles, so the -proofs do not depend on the storage-layout order solc picks. `view` constructs -a real `Storage` structure from those handles. --/ - -namespace Contracts.VaultFromSolidity.Proofs.ExecutionProof -open Verity -open Verity.Stdlib.Math -open Verity.Core.Invariant -open Verity.Proofs.Stdlib.SolidityImport -open Spec - -/-! ## Successful calls meet the named-storage spec -/ - -theorem deposit_success_spec (s post : ContractState) (amount : Uint256) - (h : (deposit amount).run s = .success () post) : - deposit_spec amount s.sender (view s) (view post) := by - unfold deposit_spec - by_cases h0 : s.msgValue = 0 - · by_cases hbal : ((view s).shareBalances s.sender).val + amount.val ≤ Verity.Core.MAX_UINT256 - · by_cases hat : (view s).totalAssets.val + amount.val ≤ Verity.Core.MAX_UINT256 - · by_cases hsup : (view s).totalSupply.val + amount.val ≤ Verity.Core.MAX_UINT256 - · solidity_simp - subst h - solidity_simp - · solidity_simp - · solidity_simp - · solidity_simp - · solidity_simp - -theorem withdraw_success_spec (s post : ContractState) (amount : Uint256) - (h : (withdraw amount).run s = .success () post) : - withdraw_spec amount s.sender (view s) (view post) := by - unfold withdraw_spec - by_cases h0 : s.msgValue = 0 - · by_cases hbal : amount.val ≤ ((view s).shareBalances s.sender).val - · by_cases hat : amount.val ≤ (view s).totalAssets.val - · by_cases hsup : amount.val ≤ (view s).totalSupply.val - · solidity_simp - subst h - solidity_simp - · solidity_simp - · solidity_simp - · solidity_simp - · solidity_simp - -theorem balance_success_spec (s post : ContractState) (account : Address) (r : Uint256) - (h : (balanceOf account).run s = .success r post) : - balanceOf_spec account r (view s) ∧ post = s := by - unfold balanceOf_spec - by_cases h0 : s.msgValue = 0 - · solidity_simp - have hr := h.1 - have heq := h.2 - subst heq - exact Eq.symm hr - · solidity_simp - -/-! ## Each entry point meets its named-storage spec -/ - -/-- `balanceOf` succeeds, leaves the state untouched, and returns the -account's shares as `balanceOf_spec` promises. -/ -theorem balance_meets_spec (s : ContractState) (account : Address) - (h0 : s.msgValue = 0) : - ∃ result, (balanceOf account).run s = ContractResult.success result s ∧ - balanceOf_spec account result (view s) := by - unfold balanceOf_spec - solidity_simp - -/-- Under `depositFits`, `deposit` succeeds and its post-state satisfies -`deposit_spec`. -/ -theorem deposit_meets_spec (s : ContractState) (amount : Uint256) - (h0 : s.msgValue = 0) (hfits : depositFits amount s.sender (view s)) : - ∃ post, (deposit amount).run s = ContractResult.success () post ∧ - deposit_spec amount s.sender (view s) (view post) := by - unfold deposit_spec - solidity_simp [depositFits] - -/-- Under `withdrawCovered`, `withdraw` succeeds and its post-state satisfies -`withdraw_spec`. -/ -theorem withdraw_meets_spec (s : ContractState) (amount : Uint256) - (h0 : s.msgValue = 0) (hcovered : withdrawCovered amount s.sender (view s)) : - ∃ post, (withdraw amount).run s = ContractResult.success () post ∧ - withdraw_spec amount s.sender (view s) (view post) := by - unfold withdraw_spec - solidity_simp [withdrawCovered] - -/-! ## The vault stays solvent -/ - -/-- Every public entry point preserves one-for-one backing of issued shares. -The six `rcases` disjuncts are the importer-generated `step` constructors in -source order (functions, then public getters), so a new entry point breaks this -theorem. Rollback is covered by `run_snd_cases`. -/ -theorem solvent_invariant : PreservedBy (fun s => solvent (view s)) step := by - intro s s' hsolvent hstep - rcases hstep with hdep | hwd | hbal | hta | hts | hsb - · rcases hdep with ⟨a, rfl⟩ - cases run_snd_cases (deposit a) s with - | inl hrev => rw [hrev]; exact hsolvent - | inr hsucc => - rcases hsucc with ⟨_, post, hrun, hsnd⟩ - rw [hsnd] - have hspec := deposit_success_spec s post a hrun - unfold solvent at hsolvent ⊢ - rcases hspec with ⟨hassets, hsupply, _⟩ - rw [hassets, hsupply, hsolvent] - · rcases hwd with ⟨a, rfl⟩ - cases run_snd_cases (withdraw a) s with - | inl hrev => rw [hrev]; exact hsolvent - | inr hsucc => - rcases hsucc with ⟨_, post, hrun, hsnd⟩ - rw [hsnd] - have hspec := withdraw_success_spec s post a hrun - unfold solvent at hsolvent ⊢ - rcases hspec with ⟨hassets, hsupply, _⟩ - rw [hassets, hsupply, hsolvent] - · rcases hbal with ⟨k, rfl⟩ - cases run_snd_cases (balanceOf k) s with - | inl hrev => rw [hrev]; exact hsolvent - | inr hsucc => - rcases hsucc with ⟨r, post, hrun, hsnd⟩ - rw [hsnd, (balance_success_spec s post k r hrun).2] - exact hsolvent - · subst hta - by_cases h0 : s.msgValue = 0 <;> solidity_simp [solvent] - · subst hts - by_cases h0 : s.msgValue = 0 <;> solidity_simp [solvent] - · rcases hsb with ⟨k, rfl⟩ - by_cases h0 : s.msgValue = 0 <;> solidity_simp [solvent] - -end Contracts.VaultFromSolidity.Proofs.ExecutionProof diff --git a/Contracts/VaultFromSolidity/Spec.lean b/Contracts/VaultFromSolidity/Spec.lean deleted file mode 100644 index 426f5cb97c..0000000000 --- a/Contracts/VaultFromSolidity/Spec.lean +++ /dev/null @@ -1,68 +0,0 @@ -import Verity.EVM.Uint256 -import Contracts.VaultFromSolidity.VaultFromSolidity - -/-! -# What the imported Vault is supposed to do - -`Importer.lean` turns `Vault.sol` into ordinary Verity definitions: `deposit`, -`withdraw`, `balanceOf`, and a read-only storage view named after the Solidity -state variables. For `v : Storage`, `v.totalAssets`, `v.totalSupply` and -`v.shareBalances` read those variables; `view s` is the view of a state `s`. -The storage-layout slot behind each name comes from solc, not from this file, -so reordering the Solidity declarations does not change anything here. - -This file only states the promise: - -* `solvent` -- the property that matters for the contract as a whole. Shares are - issued one-for-one against assets, so every share outstanding must stay backed - by an asset the vault accounts for. -* `deposit_spec` / `withdraw_spec` / `balanceOf_spec` -- what each entry point - does to the named storage, under the preconditions `depositFits` / - `withdrawCovered`. --/ - -namespace Contracts.VaultFromSolidity.Spec - -open Verity -open Verity.EVM.Uint256 - -/-- The vault's main invariant: issued shares are exactly backed by assets. -If this ever breaks, shares stop being redeemable one-for-one and the vault is -insolvent. -/ -def solvent (v : Storage) : Prop := - v.totalAssets = v.totalSupply - -/-- `deposit(amount)` credits the caller's shares and both totals by `amount`, -and leaves every other account's shares alone. -/ -def deposit_spec (amount : Uint256) (caller : Address) (pre post : Storage) : Prop := - post.totalAssets = pre.totalAssets + amount ∧ - post.totalSupply = pre.totalSupply + amount ∧ - post.shareBalances caller = pre.shareBalances caller + amount ∧ - ∀ other, other ≠ caller → post.shareBalances other = pre.shareBalances other - -/-- `withdraw(amount)` debits the caller's shares and both totals by `amount`, -and leaves every other account's shares alone. -/ -def withdraw_spec (amount : Uint256) (caller : Address) (pre post : Storage) : Prop := - post.totalAssets = pre.totalAssets - amount ∧ - post.totalSupply = pre.totalSupply - amount ∧ - post.shareBalances caller = pre.shareBalances caller - amount ∧ - ∀ other, other ≠ caller → post.shareBalances other = pre.shareBalances other - -/-- `balanceOf(account)` returns the account's shares. -/ -def balanceOf_spec (account : Address) (result : Uint256) (v : Storage) : Prop := - result = v.shareBalances account - -/-- A deposit of `amount` overflows none of the three counters it increments. -/ -def depositFits (amount : Uint256) (caller : Address) (v : Storage) : Prop := - (v.shareBalances caller).val + amount.val ≤ Verity.Core.MAX_UINT256 ∧ - v.totalAssets.val + amount.val ≤ Verity.Core.MAX_UINT256 ∧ - v.totalSupply.val + amount.val ≤ Verity.Core.MAX_UINT256 - -/-- A withdrawal of `amount` is covered by the caller's shares and both totals, -so none of the three guards in `withdraw` reverts. -/ -def withdrawCovered (amount : Uint256) (caller : Address) (v : Storage) : Prop := - amount.val ≤ (v.shareBalances caller).val ∧ - amount.val ≤ v.totalAssets.val ∧ - amount.val ≤ v.totalSupply.val - -end Contracts.VaultFromSolidity.Spec diff --git a/Contracts/VaultFromSolidity/Vault.sol b/Contracts/VaultFromSolidity/Vault.sol deleted file mode 100644 index 577aa14120..0000000000 --- a/Contracts/VaultFromSolidity/Vault.sol +++ /dev/null @@ -1,42 +0,0 @@ -// SPDX-License-Identifier: MIT -pragma solidity ^0.8.33; - -/// @title Vault -/// @notice Minimal ERC4626-style vault with 1:1 asset/share accounting. -/// @dev Source contract for the proof-only Solidity importer example. -contract Vault { - uint256 public totalAssets; - uint256 public totalSupply; - mapping(address => uint256) public shareBalances; - - error InsufficientShares(); - error InsufficientAssets(); - error InsufficientSupply(); - - function deposit(uint256 assets) external { - shareBalances[msg.sender] += assets; - totalAssets += assets; - totalSupply += assets; - } - - function withdraw(uint256 shares) external { - uint256 currentShares = shareBalances[msg.sender]; - if (currentShares < shares) { - revert InsufficientShares(); - } - if (totalAssets < shares) { - revert InsufficientAssets(); - } - if (totalSupply < shares) { - revert InsufficientSupply(); - } - - shareBalances[msg.sender] = currentShares - shares; - totalAssets -= shares; - totalSupply -= shares; - } - - function balanceOf(address account) external view returns (uint256) { - return shareBalances[account]; - } -} diff --git a/Contracts/VaultFromSolidity/VaultFromSolidity.lean b/Contracts/VaultFromSolidity/VaultFromSolidity.lean deleted file mode 100644 index b0050628c1..0000000000 --- a/Contracts/VaultFromSolidity/VaultFromSolidity.lean +++ /dev/null @@ -1,7 +0,0 @@ -import Contracts.VaultFromSolidity.Importer.Importer - -namespace Contracts - -solidity_contract VaultFromSolidity from "Vault.sol" - -end Contracts diff --git a/Dockerfile b/Dockerfile deleted file mode 100644 index d718b5c9a2..0000000000 --- a/Dockerfile +++ /dev/null @@ -1,10 +0,0 @@ -FROM debian:latest -SHELL ["/bin/bash", "-c"] -RUN apt-get update && apt-get -y install build-essential git curl sudo python3 -RUN useradd -ms /bin/bash verity && \ - echo '%sudo ALL=(ALL) NOPASSWD:ALL' >> /etc/sudoers && \ - usermod -aG sudo verity -USER verity -WORKDIR /home/verity -COPY --chown=verity:verity . . -RUN make setup && source ~/.elan/env && make verify diff --git a/Makefile b/Makefile index b58b4b55fc..fb805f721d 100644 --- a/Makefile +++ b/Makefile @@ -3,7 +3,7 @@ # Prerequisites: curl, git, python3, bash # Run `make setup` to install all tooling, then `make verify` to check all proofs. -.PHONY: help setup setup-elan setup-solc setup-solc-importer check-solc-published setup-foundry \ +.PHONY: help setup setup-elan setup-solc check-solc-published setup-foundry \ verify verify-packages verify-targeted profile-lean test test-foundry test-python axiom-report \ compile generate-yul check checks test-evmyullean-fork \ refresh-status all clean @@ -23,7 +23,7 @@ help: ## Show this help # Setup # --------------------------------------------------------------------------- -setup: setup-elan setup-solc setup-solc-importer setup-foundry ## Install all tooling (elan, solc, foundry) +setup: setup-elan setup-solc setup-foundry ## Install all tooling (elan, solc, foundry) @echo "" @echo "Setup complete. Run 'make verify' to check all proofs." @@ -49,9 +49,6 @@ setup-solc: ## Install solc (SHA256-verified) echo "solc $(SOLC_VERSION) installed"; \ fi -setup-solc-importer: ## Install official solc for the Lean Solidity importer - python3 scripts/setup_solc_importer.py - check-solc-published: ## Fetch official list.json and verify committed SHA-256 pins python3 scripts/check_solc_pin.py --verify-published-checksums @@ -138,6 +135,7 @@ check: ## Run local CI-equivalent checks job (no Lean build, no solc) python3 scripts/lean_lint.py --only trust_surface_registry python3 scripts/check_benchmark_cases.py python3 scripts/check_storage_lens_freeze.py + python3 scripts/check_feature_ownership.py python3 scripts/generate_verification_status.py --check python3 scripts/generate_layer2_boundary_catalog.py --check python3 scripts/check_verification_status_doc.py @@ -171,7 +169,6 @@ check: ## Run local CI-equivalent checks job (no Lean build, no solc) python3 scripts/generate_print_axioms.py --check python3 scripts/generate_trust_surface_report.py --check python3 scripts/lean_lint.py --only proof_length - python3 scripts/lean_lint.py --only spec_named_storage python3 scripts/check_issue_1060_integrity.py python3 scripts/update_doc_numbers.py --check python3 -m unittest discover -s scripts -p 'test_*.py' -v diff --git a/PrintAxioms.lean b/PrintAxioms.lean index aa7728b806..94bc97f4c5 100644 --- a/PrintAxioms.lean +++ b/PrintAxioms.lean @@ -4,9 +4,6 @@ import Compiler.Proofs.YulGeneration.Backends.EvmYulLeanNativeStepLemmas import Compiler.CompilationModel.ReservedScratchNames -import Contracts.SolidityImportSmoke.Inheritance.Proofs -import Contracts.SolidityImportSmoke.Modifiers.Proofs -import Contracts.SolidityImportSmoke.Structs.Proofs import Contracts.Counter.Proofs.Basic import Contracts.Counter.Proofs.Correctness import Contracts.Counter.Proofs.Preview @@ -38,7 +35,6 @@ import Contracts.SimpleToken.Proofs.Isolation import Contracts.SimpleToken.Proofs.Supply import Contracts.Vault.Proofs.Correctness import Contracts.Vault.Proofs.Native -import Contracts.VaultFromSolidity.Proofs.ExecutionProof import Verity.Proofs.CheckedExternalCallConsumer import Verity.Proofs.LoopSimulationResultAware import Verity.Proofs.Model.CommonExternalCallEquivalence @@ -47,7 +43,6 @@ import Verity.Proofs.Stdlib.Int256 import Verity.Proofs.Stdlib.ListSum import Verity.Proofs.Stdlib.MappingAutomation import Verity.Proofs.Stdlib.Math -import Verity.Proofs.Stdlib.SolidityImport import Compiler.Proofs.AbiDynamicEventObservable import Compiler.Proofs.AbiEncoding import Compiler.Proofs.AbiEventObservable @@ -296,32 +291,6 @@ end Verity.AxiomAudit Compiler.CompilationModel.compatScratch_startsWith_reserved Compiler.CompilationModel.compatScratch_not_internalImmutable - -- Contracts/SolidityImportSmoke/Inheritance/Proofs.lean - Contracts.SolidityImportSmoke.Inheritance.Proofs.pause_success_spec - Contracts.SolidityImportSmoke.Inheritance.Proofs.go_success_spec - Contracts.SolidityImportSmoke.Inheritance.Proofs.bump_success_spec - Contracts.SolidityImportSmoke.Inheritance.Proofs.dispatch_is_child - Contracts.SolidityImportSmoke.Inheritance.Proofs.super_runs_parent - Contracts.SolidityImportSmoke.Inheritance.Proofs.pause_meets_spec - Contracts.SolidityImportSmoke.Inheritance.Proofs.go_meets_spec - Contracts.SolidityImportSmoke.Inheritance.Proofs.bump_meets_spec - Contracts.SolidityImportSmoke.Inheritance.Proofs.paused_invariant - - -- Contracts/SolidityImportSmoke/Modifiers/Proofs.lean - Contracts.SolidityImportSmoke.Modifiers.Proofs.status_restored - Contracts.SolidityImportSmoke.Modifiers.Proofs.early_meets_spec - Contracts.SolidityImportSmoke.Modifiers.Proofs.guarded_success_spec - Contracts.SolidityImportSmoke.Modifiers.Proofs.guarded_reverts_not_owner_before_pause - Contracts.SolidityImportSmoke.Modifiers.Proofs.tagged_uses_base_helper - Contracts.SolidityImportSmoke.Modifiers.Proofs.snapshot_restores_status - - -- Contracts/SolidityImportSmoke/Structs/Proofs.lean - Contracts.SolidityImportSmoke.Structs.Proofs.set_then_get - Contracts.SolidityImportSmoke.Structs.Proofs.other_member_unchanged_thm - Contracts.SolidityImportSmoke.Structs.Proofs.get_meets_spec - Contracts.SolidityImportSmoke.Structs.Proofs.make_encodes - Contracts.SolidityImportSmoke.Structs.Proofs.makeRev_encodes - -- Contracts/Counter/Proofs/Basic.lean Contracts.Counter.Proofs.setStorage_updates_count Contracts.Counter.Proofs.getStorage_reads_count @@ -721,15 +690,6 @@ end Verity.AxiomAudit Contracts.Vault.Proofs.Native.vaultMinimal_runtime_lowers_native Contracts.Vault.Proofs.Native.vaultMinimal_totalAssets_nativeResultsMatchOn_revert_of_nonzero_value - -- Contracts/VaultFromSolidity/Proofs/ExecutionProof.lean - Contracts.VaultFromSolidity.Proofs.ExecutionProof.deposit_success_spec - Contracts.VaultFromSolidity.Proofs.ExecutionProof.withdraw_success_spec - Contracts.VaultFromSolidity.Proofs.ExecutionProof.balance_success_spec - Contracts.VaultFromSolidity.Proofs.ExecutionProof.balance_meets_spec - Contracts.VaultFromSolidity.Proofs.ExecutionProof.deposit_meets_spec - Contracts.VaultFromSolidity.Proofs.ExecutionProof.withdraw_meets_spec - Contracts.VaultFromSolidity.Proofs.ExecutionProof.solvent_invariant - -- Verity/Proofs/CheckedExternalCallConsumer.lean Verity.Proofs.CheckedExternalCallConsumer.lido_submit_entry_installs_caller_context Verity.Proofs.CheckedExternalCallConsumer.lido_submit_success_world @@ -1190,10 +1150,6 @@ end Verity.AxiomAudit Verity.Proofs.Stdlib.Math.safeMul_result_bounded Verity.Proofs.Stdlib.Math.safeDiv_result_le_numerator - -- Verity/Proofs/Stdlib/SolidityImport.lean - Verity.Proofs.Stdlib.SolidityImport.run_snd_cases - Verity.Proofs.Stdlib.SolidityImport.uint256_eq_zero_iff - -- Compiler/Proofs/AbiDynamicEventObservable.lean Compiler.Proofs.AbiDynamicEventObservable.evalIRExprs_append Compiler.Proofs.AbiDynamicEventObservable.evalIRExprs_map_normalizeEventWord @@ -7626,4 +7582,4 @@ end Verity.AxiomAudit Compiler.Proofs.YulGeneration.YulTransaction.ofIR_args ] --- Total: 7040 theorems/lemmas (5027 public, 2013 private, 0 sorry'd) +-- Total: 7011 theorems/lemmas (4998 public, 2013 private, 0 sorry'd) diff --git a/README.md b/README.md index 6694d4e27b..e10055463f 100644 --- a/README.md +++ b/README.md @@ -1,184 +1,46 @@ -

- Verity -

+# Verity -

Verity

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

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

+## Guarantees -

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

+- **Layer 1** (DSL → `CompilationModel`): per-contract bridge theorems. +- **Layer 2** (`CompilationModel` → IR): a generic whole-contract theorem covers the supported fragment with zero axioms. +- **Layer 3** (IR → Yul): every statement form is proved equivalent. The dispatch bridge is an explicit theorem hypothesis, not an axiom. ---- - -**Verity** is a formally verified smart contract compiler written in [Lean 4](https://lean-lang.org/). You write contracts in an embedded DSL, state what they should do, prove those properties hold, and compile to EVM bytecode. The compiler itself is proven to preserve semantics across three verified layers. Full documentation lives at [**veritylang.com**](https://veritylang.com). - -## Proof-only Solidity Vault import (POC) - -`Contracts/VaultFromSolidity/VaultFromSolidity.lean` imports the colocated -`Vault.sol` with `solidity_contract VaultFromSolidity from "Vault.sol"`. -The Lean frontend invokes pinned solc 0.8.33 for typed AST and storage layout, -validates them, and parses them into the closed, intrinsically typed inductive in -`Contracts/VaultFromSolidity/Importer/Syntax.lean`. `Importer/Semantics.lean` -gives each construct its one meaning, and each entry point is registered in -memory as `Fn.meaning` applied to its parsed term, as a transparent, -kernel-checked `Verity.Contract` definition. There is no Python -frontend, custom serialized IR, generated `.lean`, CompilationModel, or -bytecode. The example is independent of the -handwritten `Contracts/Vault` contract. - -The importer elaborates a kernel-checked `Storage` structure named after the -Solidity state variables, so `Spec.lean` reads like the contract instead of -naming raw slots: - -```lean --- before -def solvent (s : ContractState) : Prop := s.readSlot 0 = s.readSlot 1 - --- after -def solvent (v : Storage) : Prop := v.totalAssets = v.totalSupply - -def deposit_spec (amount : Uint256) (caller : Address) (pre post : Storage) : Prop := - post.totalAssets = pre.totalAssets + amount ∧ - post.totalSupply = pre.totalSupply + amount ∧ - post.shareBalances caller = pre.shareBalances caller + amount ∧ - ∀ other, other ≠ caller → post.shareBalances other = pre.shareBalances other -``` - -`view` constructs that `Storage` from the `Slot` handles solc's storage -layout produced, so reordering the Solidity declarations moves the slots without -touching the spec, and renaming a variable makes the spec fail to elaborate. -`#print view` shows the `readSlot`/`readMap` unfolding. The importer also -registers a deterministic entry-point relation `step`. `Proofs/ExecutionProof.lean` -proves each successful call meets its spec and that `solvent` is preserved by -`step` (`solvent_invariant`). - -With the Lean/package prerequisites installed, put an official solc 0.8.33 -build at `.lake/solidity-import/solc` (`make setup-solc-importer` fetches the -host platform's `list.json` from binaries.soliditylang.org, checks the -published SHA-256 against the committed pin, and installs that binary). The -importer allowlists the official linux-amd64 and macosx-amd64 digests; Linux -CI still uses linux-amd64. Then run: - -```sh -lake build VaultFromSolidity -python3 Contracts/VaultFromSolidity/Importer/scripts/solidity_importer_test.py -lake build SolidityImportSmokeInheritance -python3 Contracts/SolidityImportSmoke/Inheritance/scripts/inheritance_test.py -lake build SolidityImportSmokeModifiers -python3 Contracts/SolidityImportSmoke/Modifiers/scripts/modifiers_test.py -lake build SolidityImportSmokeStructs -python3 Contracts/SolidityImportSmoke/Structs/scripts/structs_test.py -``` - -The Vault acceptance script uses disposable copies for source mutations, fail-closed -rejection, content-based Lake freshness, compiler/importer/build-policy -invalidation, declaration-registration rollback, and an audit of every Vault -theorem. It never mutates the original Solidity file. The inheritance smoke -(`Contracts/SolidityImportSmoke/Inheritance`) covers same-file `is` bases, C3 -linearization including a diamond, virtual dispatch, `super` (target C3, not -the defining-contract AST id), opaque fields, and internal calls (`Expr.call` -is view/pure only); `inheritance_test.py` is the matching focused suite. The -modifiers smoke inlines argument-free modifiers in declaration order, including -`nonReentrant` postludes after an early `return` (`super` in a modifier uses -the modifier's defining contract; a prelude `return` before `_` is rejected). -The structs smoke encodes and decodes a two-member user struct used as a -parameter, storage field, and return (canonical names, one-shot RHS, zero -defaults, public product getter, struct ids in signatures). -Save Solidity, rebuild this dedicated target, then reload the Lean editor: -an already-open editor snapshot does not automatically watch `.sol` changes. -See [the trust boundary](TRUST_ASSUMPTIONS.md#proof-only-solidity-vault-import). - -## Verification status - -All proofs are machine-checked by the Lean kernel. CI rebuilds the proof development on every commit, and repository checks enforce that no proof is left incomplete (no `sorry`) and that the compiler proof stack carries 0 axioms (see [AXIOMS.md](AXIOMS.md)). Verification is scoped rather than total: the generic compiler theorems cover an explicitly documented fragment of the language, and the precise boundary between what is proven and what is trusted is maintained in [TRUST_ASSUMPTIONS.md](TRUST_ASSUMPTIONS.md). - -A detailed proof inventory — theorem status, per-contract results, and test coverage — is regenerated from the codebase and tracked in [docs/VERIFICATION_STATUS.md](docs/VERIFICATION_STATUS.md). All checks are reproducible locally (see [Quick start](#quick-start)). - -## What is verified - -Verity proves that compilation preserves behavior at three stages. Each layer is a machine-checked Lean theorem. - -**Layer 1** (EDSL to CompilationModel): the `verity_contract` macro generates both an executable Lean program and a compiler-facing model from a single definition. Per-contract bridge theorems prove they agree. - -**Layer 2** (CompilationModel to IR): a generic whole-contract theorem covers the supported fragment with zero axioms. No per-contract proof effort needed. Internal helper calls now exist at the source level, and helper-summary proof reuse is available in source-semantics lemmas, but that reuse is not yet fully consumed through the generic body/IR theorem path. ECMs, typed interface calls, external calls, and low-level call/returndata mechanics are trust-reported or compiler-supported rather than fully proof-modeled. Constructors, fallback/receive, events/logs, typed errors, proxy/delegatecall, local obligations, and richer storage-layout features remain outside the generic proof fragment or partial. `forEach` support is deliberately partial: zero-bound loops with supported bodies and arbitrary literal-bound empty-body loops are proved, while positive non-empty loop bodies remain outside the current theorem. - -**Layer 3** (IR to Yul): all statement types are proven equivalent. The dispatch bridge is an explicit theorem hypothesis, not an axiom. - -The Yul-to-bytecode step is handled by `solc` (v0.8.33, pinned) and is not verified by Verity. See [TRUST_ASSUMPTIONS.md](TRUST_ASSUMPTIONS.md) for the complete trust boundary. - -## How it works - -``` -verity_contract Counter where - storage count : Uint256 := slot 0 - function increment () : Unit := do - let current <- getStorage count - setStorage count (add current 1) -``` - -```lean -theorem increment_correct (s : ContractState) : - let s' := ((increment).run s).snd - s'.storage 0 = add (s.storage 0) 1 := by rfl -``` - -The proof passes by `rfl` (reflexivity): Lean's kernel evaluates both sides and confirms they are definitionally equal. No external solver, no bounded model checker, no trust in anything beyond Lean's type theory. +The compiler proof stack carries 0 axioms and no `sorry`. Yul → bytecode is +done by pinned `solc` and is trusted. Verification is scoped: the proved +fragment and everything trusted are listed in the trust documents below. ## Quick start -```bash -curl https://raw.githubusercontent.com/leanprover/elan/master/elan-init.sh -sSf | sh -source ~/.elan/env -git clone https://github.com/lfglabs-dev/verity.git && cd verity -lake build # verify all proofs (~20 min first build) -make check # run full CI validation suite -FOUNDRY_PROFILE=difftest forge test # differential tests: EDSL vs EVM +```sh +lake build # check every proof +make check # repository checks run in CI (no Lean build) +make test-foundry ``` -## How Verity compares - -| | Certora | Halmos | Verity | -|---|---|---|---| -| **Approach** | Bounded model checking | Symbolic execution | Theorem proving in Lean 4 | -| **Proof scope** | Bounded (configurable depth) | Bounded (path explosion) | Unbounded (all inputs, all paths) | -| **Compiler trust** | Trusts solc entirely | Trusts solc entirely | Verifies 3 compilation layers | -| **Best for** | Production audits at scale | Bug-finding in Foundry | High-assurance contracts | - -Verity is complementary to these tools. It is for cases where you need mathematical certainty across all inputs and all execution paths. - -## Documentation - -| Resource | Description | -|----------|-------------| -| [veritylang.com](https://veritylang.com/) | Full documentation site | -| [Solidity to Verity](https://veritylang.com/guides/solidity-to-verity) | Practical syntax and semantic mappings for Solidity ports | -| [Production Solidity Patterns](https://veritylang.com/guides/production-solidity-patterns) | Agent guidance for production ports, reusable Verity features, and oracle/spec boundaries | -| [docs/ARCHITECTURE.md](docs/ARCHITECTURE.md) | Contributor map for source features, proof layers, trust surfaces, and regression checks | -| [docs/VERIFICATION_STATUS.md](docs/VERIFICATION_STATUS.md) | Theorem counts, proof status, test coverage | -| [docs/INTRINSICS.md](docs/INTRINSICS.md) | Consumer-owned opcode bindings and their trust model | -| [docs/LOW_LEVEL_YUL.md](docs/LOW_LEVEL_YUL.md) | Policy for typed low-level primitives vs. raw Yul escape hatches | -| [TRUST_ASSUMPTIONS.md](TRUST_ASSUMPTIONS.md) | What is verified vs. what is trusted | -| [AXIOMS.md](AXIOMS.md) | Documented axioms (currently 0) | -| [AUDIT.md](AUDIT.md) | Trust-boundary audit evidence and CI guards | -| [CONTRIBUTING.md](CONTRIBUTING.md) | Contribution guidelines | - -### Research +## Where things are -- [Verity: A Formally Verified Smart Contract Compiler](https://lfglabs.dev/papers/verity.pdf) -- [Verity Benchmark: AI-Driven Proof Generation](https://lfglabs.dev/research/verity-benchmark) -- [What is a formal proof?](https://lfglabs.dev/research/what-is-a-formal-proof) +| Path | Contents | +| --- | --- | +| `Verity/` | The DSL (`verity_contract`), its semantics and proof library | +| `Compiler/` | The compiler and its correctness proofs | +| `Compiler/SolidityImport/` | Solidity → `CompilationModel` importer ([docs](docs/SOLIDITY_IMPORT.md)) | +| `Contracts/` | Example and smoke contracts with their proofs | +| `docs/` | Trust boundary, axioms, verification status, design notes | +| `scripts/`, `test/`, `artifacts/` | CI checks, Foundry tests, generated reports | -## Support +## What to review -Verity is a public good. If you'd like to support the project, you can donate via Giveth: +1. [docs/TRUST_ASSUMPTIONS.md](docs/TRUST_ASSUMPTIONS.md): what is proved and what is trusted. +2. [docs/AXIOMS.md](docs/AXIOMS.md): every axiom, with its justification. +3. [docs/VERIFICATION_STATUS.md](docs/VERIFICATION_STATUS.md): theorem and test inventory, regenerated by CI. +4. [docs/SOLIDITY_IMPORT.md](docs/SOLIDITY_IMPORT.md): the Solidity importer and its differential validation. -[verity: Smart Contracts Security for the Age of AI](https://giveth.io/project/verity:-smart-contracts-security-for-the-age-of-ai) +Contributions: see [CONTRIBUTING.md](CONTRIBUTING.md). diff --git a/SUPPORTEDSPEC_CONVERSION.md b/SUPPORTEDSPEC_CONVERSION.md deleted file mode 100644 index ec22e544a9..0000000000 --- a/SUPPORTEDSPEC_CONVERSION.md +++ /dev/null @@ -1,193 +0,0 @@ -# `SupportedSpec.lean` → compiler-call conversion - -`Compiler/Proofs/IRGeneration/SupportedSpec.lean` historically hand-restates the -set of programs the compiler accepts as standalone `Prop`s or pattern-matching -`Bool`s. That style duplicates compiler logic: every time the compiler relaxes -or tightens a gate, the proof layer's enumeration silently drifts and the -"supported" envelope quietly diverges from what the compiler actually accepts. - -The repo already established a better pattern in the event-emission predicates, -where the proof-side support predicate is **defined by calling the actual -compiler gating function**, so the two cannot disagree by construction: - -```lean --- Compiler side (Compiler/CompilationModel/EventEmission.lean) -def eventParamScalarCompileSupported (ty : ParamType) : Bool := ... - --- Proof side (this file) -def eventParamScalarProofSupported (ty : ParamType) : Bool := - eventParamScalarCompileSupported ty -``` - -The proof-side name signals **intent** ("what proofs depend on for soundness"), -while the body delegates to the compile-side Bool that drives the compiler's -real branch. Downstream lemmas like -`eventParamScalarProofSupported_eventIsDynamicType_eq_false` then `simp` through -both names interchangeably. - -This PR converts two scalar/leaf predicates in `SupportedSpec.lean` to the same -shape, as an exemplar that the remaining ~70 hand-restated predicates in the -file can follow. - -## Predicates converted - -### 1. `SupportedExternalScalarParamType` → `externalParamScalarProofSupported` - -The hand-restated `Prop`: - -```lean -def SupportedExternalScalarParamType : ParamType → Prop - | .uint256 | .int256 | .uint8 | .uint16 | .address | .bytes32 | .bool => True - | _ => False -``` - -is the exact mirror of the compiler-side gating function in -`Compiler/CompilationModel/ParamLoading.lean`: - -```lean -def isScalarParamType : ParamType → Bool - | ParamType.uint256 | ParamType.int256 | ParamType.uint8 | ParamType.uint16 - | ParamType.address | ParamType.bool | ParamType.bytes32 => true - | _ => false -``` - -The new proof-side alias is one line: - -```lean -def externalParamScalarProofSupported (ty : ParamType) : Bool := - isScalarParamType ty -``` - -### 2. `SupportedExternalReturnProfile` → `externalReturnProfileProofSupported` - -The hand-restated `Prop`: - -```lean -def SupportedExternalReturnProfile : List ParamType → Prop - | [] => True - | [ty] => SupportedExternalScalarParamType ty - | _ => False -``` - -becomes a Bool that pushes the per-element decision down to the compiler: - -```lean -def externalReturnProfileProofSupported (returns : List ParamType) : Bool := - decide (returns.length ≤ 1) && returns.all isScalarParamType -``` - -## Agreement theorems - -Because the heavily-used hand-restated predicates currently sit in proof -preconditions (`∀ param ∈ params, SupportedExternalScalarParamType param.ty`) across -many sibling modules, we **keep** the old defs and prove -`old ↔ new` instead of rewriting every caller in one shot. The biconditionals -are the meaning-preservation oracle: - -```lean -theorem SupportedExternalParamType_iff_externalParamScalarProofSupported - (ty : ParamType) : - SupportedExternalScalarParamType ty ↔ externalParamScalarProofSupported ty = true - -theorem SupportedExternalReturnProfile_iff_externalReturnProfileProofSupported - (returns : List ParamType) : - SupportedExternalReturnProfile returns ↔ - externalReturnProfileProofSupported returns = true -``` - -Both proofs are a single `cases ... <;> simp [...]` / `match ... with => simp` -because the cases line up. No `sorry`, no new `axiom`, no `native_decide`. - -## Recipe to replicate across remaining predicates - -For each hand-restated proof-side predicate `P` in `SupportedSpec.lean` (or a -sibling proof module): - -1. **Find the compiler-side gating function.** Grep the `CompilationModel` - directory for a `Bool` (or `Except` / `Option`-returning) function whose - match arms classify the same syntactic cases as `P`. For ParamType-level - leaves the usual suspects are `isScalarParamType`, - `isSingleWordStaticParamType`, `isDynamicParamType`, `isWordArrayParam`, - `internalDynamicParamSupported`, `supportedCustomErrorParamType`, - `eventParamScalarCompileSupported`, and `indexedDynamicArrayElemSupported`. - For expression / statement predicates the gate is typically the `compile*` - function whose `Except String` failure path encodes the unsupported cases — - in which case the proof-side wrapper checks `.isOk` / `.isSome`. - -2. **Introduce a thin proof-side wrapper.** Add a single-line - `def fooProofSupported ... := compilerGate ...` whose body is just the - compiler call. The naming convention is `ProofSupported` - to match `eventParamScalarProofSupported`. Place it near the existing - predicates so the pattern is locally visible. - -3. **Prove agreement with the hand-restated form.** Add - `theorem P_iff_fooProofSupported : P x ↔ fooProofSupported x = true`. For - leaf scalar/leaf predicates this is a one-liner: - `cases x <;> simp [P, fooProofSupported, compilerGate]`. For predicates - over lists or composite IR, recurse with `match`/`induction` and reuse the - leaf agreement lemma. For `Except String`-shaped gates, the agreement - lemma is `P x ↔ (compilerGate x).isOk` and the proof works by case-splitting - on the `Except` result. - -4. **Decide on retention.** If `P` is referenced widely outside the file (use - `grep -rn P --include='*.lean'`), keep `P` and only add the agreement - theorem — the lemma is enough to feed both directions into existing proofs. - If `P` has few callers, inline-replace each caller with the new wrapper and - delete `P`; the agreement theorem then degrades into a self-test you can - keep or drop. - -5. **Build the affected module.** `lake build - Compiler.Proofs.IRGeneration.SupportedSpec` (or the sibling module). - Iterate until clean: no `sorry`, no new `axiom`, no `native_decide`. The - typical failure modes are: - - `simp` can't close a case because the compiler bool depends on a helper - that isn't unfolded yet. Add it to the `simp` set. - - The compiler bool is strictly broader/narrower than the hand version. - This is a **drift bug** — fix the spec to match the compiler, or - restrict the new wrapper with `&& extraCondition`. - - The compiler gate is `Except`/`Option`-shaped. Don't `simp` the failure - branch away; pattern-match it and prove the failure case maps to `P x = False`. - -6. **Once a critical mass of predicates is converted**, audit the resulting - `SupportedSpec` and consider deleting the original hand-restated Props - entirely, since every call site that used `P x` now factors through - `P_iff_fooProofSupported` to talk about `fooProofSupported x = true`. - -## Module build status — BLOCKED on pre-existing breakage - -`lake build Compiler.Proofs.IRGeneration.SupportedSpec` was run after the -edits and **failed**, but not because of this conversion. The branch this work -sits on top of has a pre-existing missing-case error: a recent commit added a -new `Expr.txOrigin` variant (see commits introducing `txOrigin` in -`Compiler/CompilationModel/UsageAnalysis.lean`, `LogicalPurity.lean`, -`ValidationInterop.lean`, etc.) but the proof-side files that pattern-match -exhaustively on `Expr` were never updated. The first build failure surfaces -in `Compiler/Proofs/IRGeneration/ExprCore.lean:18` (the `exprBoundNames` -mutual block): - -``` -error: Compiler/Proofs/IRGeneration/ExprCore.lean:18:2: Missing cases: -Expr.txOrigin -error: Lean exited with code 1 -``` - -`SupportedSpec.lean` itself has approximately ten further exhaustive `Expr` -matches that would also need `.txOrigin` added (every `exprTouchesUnsupported*Surface` -function around lines 604, 754, 810, 863, 922, 990, 1053, 1114, 1176, 1896). - -Fixing all of these is outside the conversion scope this task was chartered -for (replicating the `eventParamScalarCompileSupported` pattern). The two -new defs (`externalParamScalarProofSupported`, -`externalReturnProfileProofSupported`) and the two agreement theorems were -verified by inspection — they each rely only on `simp` with the relevant -definitions in scope and follow the same recipe that's already known to -compile for `eventParamScalarProofSupported`. They contain no `sorry`, no -new `axiom`, and no `native_decide`. - -**Recommended unblock path (separate, focused PR):** sweep -`Compiler/Proofs/IRGeneration/{ExprCore.lean, SupportedSpec.lean, FunctionBody.lean, SourceSemantics.lean, ...}` -and add `.txOrigin` to every exhaustive `Expr` enumeration, mirroring the -existing `.caller`/`.contractAddress`/`.chainid` handling (zero bound names, -no helper calls, no unsupported surface). Once that lands, -`lake build Compiler.Proofs.IRGeneration.SupportedSpec` should pick up the -work here and the two agreement theorems should close as written. diff --git a/StorageLayoutReport.lean b/StorageLayoutReport.lean index b8444b2f21..9c96032ff4 100644 --- a/StorageLayoutReport.lean +++ b/StorageLayoutReport.lean @@ -14,7 +14,7 @@ inside `Compiler/`. `PrintAxioms.lean` uses the same root-level placement for the same reason. - See AUDIT.md for the audit-artifact registry. + See docs/TRUST_ASSUMPTIONS.md for the trust boundary. -/ import Compiler.CompilationModel import Compiler.CompilationModel.LayoutReport diff --git a/Verity/Compiler/FromSolidity.lean b/Verity/Compiler/FromSolidity.lean deleted file mode 100644 index 179c4669a4..0000000000 --- a/Verity/Compiler/FromSolidity.lean +++ /dev/null @@ -1,142 +0,0 @@ -import Lean - -namespace Verity.Compiler.FromSolidity - -open Lean Elab Command - -/-! -# `#fromSolidity` Lean entry point — scaffold - -Today the Verity compiler runs out-of-band via the `verity-cli` -command-line tool: a developer manually invokes -`verity-cli compile path/to/Foo.sol --emit lean` and copies the -generated Lean term into the benchmark. - -This module is the **scaffold** for surfacing that pipeline as a Lean -elaborator-level command, so that a benchmark file can declare: - -``` -#fromSolidity "vendor/account-abstraction/EntryPoint.sol" -``` - -and obtain a proof-checked `verity_contract` term as if it had been -hand-written. - -## Status - -This scaffold lands the public API surface (`#fromSolidity` command, -options, error type) and a stub elaborator that defers to the CLI via -`IO.Process.run`. The full machine translation requires hooking -`Compiler/CompilationModel.lean`'s Solidity-IR pipeline directly into -Lean's elaborator, which is a multi-week project tracked separately -(see `docs/ROADMAP.md` "machine fromSolidity"). - -The scaffold exists so: - -* The API contract is fixed (the command name and option shape will not - change underneath downstream benchmarks). -* The CLI integration point is documented as a single function - (`runVerityCliCompile`) that the next PR replaces with an in-process - call. -* Benchmarks that today write `verity_contract` blocks by hand can - switch to `#fromSolidity` once the CLI shells out cleanly, without - changing their proof files. - -## Trust boundary today vs. after full implementation - -Today (`fromSolidity` shells out to `verity-cli`): -* Trust the CLI binary on disk. -* Trust the file-system path is consistent with what the developer - proved against. - -After full implementation (in-process): -* No CLI binary trust — translation runs inside Lean. -* The same kernel checks the resulting term that checks every Lean - proof. - -The headline difference: the post-implementation form makes -*translation faithfulness* part of the proof-checked trust base. --/ - -/-- Options for `#fromSolidity`. -/ -structure Options where - /-- Path to the Solidity source. Resolved relative to the workspace root. -/ - path : System.FilePath - /-- Optional contract name selector (defaults to the only contract in the file). -/ - contractName : Option String := none - /-- Verity CLI binary to invoke. Defaults to `verity-cli` on PATH. -/ - cliBinary : String := "verity-cli" - deriving Repr - -/-- Result of running the CLI translation step. -/ -inductive Result where - | ok (lean : String) - | error (msg : String) - deriving Repr - -/-- Locate the Lake workspace root containing the current process, if any. -/ -partial def findWorkspaceRoot? (dir : System.FilePath) : IO (Option System.FilePath) := do - if ← (dir / "lakefile.lean").pathExists then - pure (some dir) - else if ← (dir / "lakefile.toml").pathExists then - pure (some dir) - else - match dir.parent with - | some parent => - if parent == dir then - pure none - else - findWorkspaceRoot? parent - | none => pure none - -/-- Resolve `path` according to the `Options.path` contract. - -Absolute paths are preserved. Relative paths are anchored at the Lake workspace -root containing the current process, falling back to the process working -directory when the command is run outside a Lake workspace. -/ -def resolveSourcePath (path : System.FilePath) : IO System.FilePath := do - if path.isAbsolute then - pure path.normalize - else - let cwd ← IO.currentDir - let root ← findWorkspaceRoot? cwd - pure (((root.getD cwd) / path).normalize) - -/-- Invoke `verity-cli compile path --emit lean`. Returns the emitted - Lean source on stdout or an error message. -/ -def runVerityCliCompile (opts : Options) : IO Result := do - let sourcePath ← resolveSourcePath opts.path - let args := match opts.contractName with - | some c => - #["compile", sourcePath.toString, "--contract", c, "--emit", "lean"] - | none => - #["compile", sourcePath.toString, "--emit", "lean"] - try - let out ← IO.Process.run { cmd := opts.cliBinary, args } - return .ok out - catch e => - return .error s!"verity-cli failed: {e.toString}" - -/-- `#fromSolidity "path"` command. Currently emits a placeholder - `axiom` whose name documents the pending translation; replace with a - proper elaborator emission once the in-process translator lands. -/ -syntax (name := fromSolidityCmd) "#fromSolidity " str (" contract " str)? : command - -@[command_elab fromSolidityCmd] -def elabFromSolidity : CommandElab := fun stx => do - let pathLit := stx[1] - let path : System.FilePath := pathLit.isStrLit?.get! - let contractOpt : Option String := - if stx[2].getNumArgs > 0 then some stx[2][1].isStrLit?.get! else none - let opts : Options := { path := path, contractName := contractOpt } - let result : Result ← (runVerityCliCompile opts).toIO - (fun e => IO.userError s!"fromSolidity IO failure: {e.toString}") - match result with - | .ok _emitted => - let sourcePath ← (resolveSourcePath opts.path).toIO - (fun e => IO.userError s!"fromSolidity path resolution failure: {e.toString}") - logInfo m!"fromSolidity scaffold: would elaborate {sourcePath}; in-process translator pending" - | .error msg => - throwError m!"fromSolidity: {msg}. Falling back: declare the verity_contract block by hand for now." - -end Verity.Compiler.FromSolidity diff --git a/Verity/Core/Model/Denote.lean b/Verity/Core/Model/Denote.lean index 9e65068978..0e8f1f5900 100644 --- a/Verity/Core/Model/Denote.lean +++ b/Verity/Core/Model/Denote.lean @@ -58,8 +58,12 @@ semantic definition. ## Outside the initial denotation fragment Exactly the constructs `SourceSemantics` itself maps to `none`/`.revert`, apart -from the memory-backed word arrays intentionally widened here: -`arrayElementDynamic*`, `paramDynamic*`, `forkIfAtLeast`, `mappingChain` +from the memory-backed word arrays intentionally widened here and +`Stmt.returnValues` (multi-word return recorded in +`DenoteState.observedReturnWords`, finished with `.stop`, and surfaced by +`denoteFunction` as `DenoteResult.returnWords` in source order). +Still outside: `arrayElementDynamic*`, `paramDynamic*`, +`forkIfAtLeast`, `mappingChain` reads with zero or three-plus keys (one/two-key reads and writes are supported), and every `Expr`/`Stmt` constructor not listed in the arms below (raw calls, ABI re-encoding returns, ECM, unsafe Yul, @@ -265,6 +269,12 @@ structure DenoteState where externalCallReturnValues : Nat → List Nat := fun _ => [] externalCallPostWorld : Nat → Option Verity.ContractState := fun _ => none externalCallIndex : Nat := 0 + /-- Words produced by `Stmt.returnValues`, in source order. + `none` means this frame has not executed a multi-value return. + The proof semantics records the words here and finishes with `.stop`; + it does not observe a Solidity panic payload. `Stmt.panic` is still a + revert, and the `PanicCode` stays on the statement for audit. -/ + observedReturnWords : Option (List Nat) := none /-- Mirrors `SourceSemantics.StmtResult`. -/ inductive StmtOutcome where @@ -1515,6 +1525,11 @@ mutual else .revert | none => .revert | _, .revertReturndata => .revert + | state, .returnValues values => + match evalExprList oracle fields state values with + | some resolved => + .stop { state with observedReturnWords := some (resolved.map wordNormalize) } + | none => .revert | _, _ => .revert def execStmtList (oracle : DenoteOracle) (fields : List Field) : @@ -1548,6 +1563,10 @@ structure DenoteTransaction where structure DenoteResult where success : Bool returnValue : Option Nat + /-- Multi-word return observed from `Stmt.returnValues`. + Empty when the frame returned a single word or no word. + Order is the source order of the return expressions. -/ + returnWords : List Nat := [] finalStorage : Nat → Nat events : List (List Nat) @@ -1565,9 +1584,11 @@ def revertedResult (oracle : DenoteOracle) (spec : CompilationModel) events := encodeEvents initialWorld.events } def successResult (oracle : DenoteOracle) (spec : CompilationModel) - (world : Verity.ContractState) (ret : Option Nat) : DenoteResult := + (world : Verity.ContractState) (ret : Option Nat) (returnWords : List Nat := []) : + DenoteResult := { success := true returnValue := ret + returnWords := returnWords finalStorage := encodeStorage oracle spec world events := encodeEvents world.events } @@ -1653,7 +1674,16 @@ def denoteFunction (oracle : DenoteOracle) (spec : CompilationModel) (fn : Funct externalCallPostWorld := externalCallPostWorld } fn.body with | .continue state => successResult oracle spec state.world none - | .stop state => successResult oracle spec state.world none + | .stop state => + -- Multi-word returns (`Stmt.returnValues`) are observable in + -- `returnWords` (source order). `returnValue` stays `none` so that + -- `toSourceResult` — whose target type has no multi-word field — + -- still agrees with `SourceSemantics.interpretFunction`, which does + -- not surface `Stmt.returnValues` payloads. No `match` on + -- `observedReturnWords` here: keeping the arm match-free lets the + -- `denoteFunction_eq` agreement proof close by `simp`. + successResult oracle spec state.world none + (returnWords := state.observedReturnWords.getD []) | .return value state => successResult oracle spec state.world (some value) | .revert => revertedResult oracle spec worldWithTx diff --git a/Verity/Core/SolidityImportAttr.lean b/Verity/Core/SolidityImportAttr.lean deleted file mode 100644 index 7c0104f77e..0000000000 --- a/Verity/Core/SolidityImportAttr.lean +++ /dev/null @@ -1,3 +0,0 @@ -import Lean - -register_simp_attr solidity_import diff --git a/Verity/EVM/Frame.lean b/Verity/EVM/Frame.lean index 84bc78db49..6890147c9a 100644 --- a/Verity/EVM/Frame.lean +++ b/Verity/EVM/Frame.lean @@ -28,7 +28,7 @@ Before this module, every benchmark that touched external calls had to assume (or reproduce in their own file) the EVM-level frame condition that external `CALL` cannot SSTORE the caller's slots and cannot write to caller memory outside the declared output buffer. That assumption is -documented in `AXIOMS.md` under "External Call Module". This module +documented in `docs/AXIOMS.md` under "External Call Module". This module makes the frame condition a theorem of `Verity.EVM`, parameterised over an abstract finite `CalleeResult` observable. diff --git a/Verity/Proofs/Stdlib/SolidityImport.lean b/Verity/Proofs/Stdlib/SolidityImport.lean deleted file mode 100644 index 80827a0da9..0000000000 --- a/Verity/Proofs/Stdlib/SolidityImport.lean +++ /dev/null @@ -1,51 +0,0 @@ -import Lean -import Verity.Core -import Verity.Core.Invariant -import Verity.Core.SolidityImportAttr -import Verity.Stdlib.Math -import Verity.EVM.Uint256 - -namespace Verity.Proofs.Stdlib.SolidityImport - -open Verity -open Lean Parser.Tactic - -/-- `Contract.run` either rolls back to the pre-state or returns a successful post-state. -/ -theorem run_snd_cases {α : Type} (c : Contract α) (s : ContractState) : - (c.run s).snd = s ∨ ∃ a post, c.run s = .success a post ∧ (c.run s).snd = post := by - unfold Contract.run - cases c s - · exact Or.inr ⟨_, _, rfl, rfl⟩ - · exact Or.inl rfl - -/-- The imported nonpayable guard tests `msgValue.val`; this relates it to `= 0`. -/ -theorem uint256_eq_zero_iff (x : Verity.Core.Uint256) : x = 0 ↔ x.val = 0 := by - constructor - · intro h; subst h; exact Verity.Core.Uint256.val_zero - · intro h; exact Verity.Core.Uint256.ext (h.trans Verity.Core.Uint256.val_zero.symm) - -syntax (name := soliditySimpTac) - "solidity_simp" (" [" simpLemma,* "]")? : tactic - -macro_rules - | `(tactic| solidity_simp) => - `(tactic| solidity_simp []) - | `(tactic| solidity_simp [$args,*]) => - `(tactic| simp_all [solidity_import, - $(mkIdent ``Contract.run):ident, $(mkIdent ``Bind.bind):ident, $(mkIdent ``Pure.pure):ident, - $(mkIdent ``Verity.instMonadContract):ident, $(mkIdent ``Verity.bind):ident, $(mkIdent ``Verity.pure):ident, - $(mkIdent ``msgValue):ident, $(mkIdent ``msgSender):ident, $(mkIdent ``Verity.require):ident, - $(mkIdent ``getStorage):ident, $(mkIdent ``setStorage):ident, $(mkIdent ``getStorageAddr):ident, - $(mkIdent ``setStorageAddr):ident, $(mkIdent ``getMapping):ident, - $(mkIdent ``setMapping):ident, $(mkIdent ``Verity.Stdlib.Math.requireSomeUint):ident, - $(mkIdent ``Verity.Stdlib.Math.safeAdd):ident, $(mkIdent ``Verity.Stdlib.Math.safeSub):ident, - $(mkIdent ``Verity.EVM.Uint256.sub):ident, $(mkIdent ``Nat.not_le_of_lt):ident, - $(mkIdent ``Nat.not_lt_of_ge):ident, $(mkIdent ``ContractState.readSlot):ident, - $(mkIdent ``ContractState.writeSlot):ident, $(mkIdent ``ContractState.readAddrSlot):ident, - $(mkIdent ``ContractState.writeAddrSlot):ident, $(mkIdent ``ContractState.readMap):ident, - $(mkIdent ``ContractState.writeMap):ident, $(mkIdent ``ContractState.storage):ident, - $(mkIdent ``ContractState.storageAddr):ident, $(mkIdent ``ContractState.storageMap):ident, - $(mkIdent ``uint256_eq_zero_iff):ident, - $args,*]) - -end Verity.Proofs.Stdlib.SolidityImport diff --git a/Verity/Trace.lean b/Verity/Trace.lean deleted file mode 100644 index 18b3a198e7..0000000000 --- a/Verity/Trace.lean +++ /dev/null @@ -1,109 +0,0 @@ -namespace Verity.Trace - -/-! -# Counting trace machinery - -A reusable type-class–based abstraction for "this event happens exactly -N times" properties over contract execution traces. Promoted from -`Benchmark.Cases.ERC4337.EntryPointInvariant.Trace`. - -## Pattern - -A `Trace` is a `List Event` for some event type. A `MatchKey` is -arbitrary metadata (an address, a calldata, a selector); a contract -trace property is typically "the trace contains exactly one event -matching this key." - -We provide: - -* `countMatching` — generic counting predicate. -* `count_le_one_under_pairwise_distinct` — at most one matching event - when the source list of events generators are pairwise distinct on the - key. - -Downstream contracts instantiate with their concrete `Event`, `Key`, -and `matchEvent` predicate. --/ - -/-- `countMatching` is positive iff some event matchEvent the key. -/ -def countMatching {Event Key : Type} - (matchEvent : Key → Event → Bool) (key : Key) (trace : List Event) : Nat := - (trace.filter (matchEvent key)).length - -/-! ## Generic at-most-once theorem - -Stated parametrically over an event-generation function `emit : α → -Option Event` (returns `none` when the generator contributes nothing, as -in the ERC-4337 `callData.length = 0` branch). The generator's domain -list `xs` is the source of truth; we case-split on it. --/ - -/-- A generic execution loop: `xs` is the list of operations, `emit` - produces zero or one `Event` per operation. -/ -def emitLoop {α Event : Type} (emit : α → Option Event) : List α → List Event - | [] => [] - | x :: rest => - match emit x with - | some e => e :: emitLoop emit rest - | none => emitLoop emit rest - -/-- Every event in `emitLoop emit xs` came from some `x ∈ xs`. -/ -theorem emitLoop_event_origin {α Event : Type} - (emit : α → Option Event) (xs : List α) (e : Event) - (he : e ∈ emitLoop emit xs) : - ∃ x ∈ xs, emit x = some e := by - induction xs with - | nil => simp [emitLoop] at he - | cons hd rest ih => - simp only [emitLoop] at he - split at he - · rename_i e' hEq - rcases List.mem_cons.mp he with hHd | hTail - · refine ⟨hd, List.mem_cons_self .., ?_⟩ - rw [hEq, hHd] - · obtain ⟨x, hx, hex⟩ := ih hTail - exact ⟨x, List.mem_cons_of_mem _ hx, hex⟩ - · obtain ⟨x, hx, hex⟩ := ih he - exact ⟨x, List.mem_cons_of_mem _ hx, hex⟩ - -/-- **At-most-once**: if all events emitted from `xs` are pairwise - distinct on a matching key, the trace contains at most one match. -/ -theorem count_le_one_under_pairwise_distinct - {α Event Key : Type} [DecidableEq Event] - (emit : α → Option Event) (matchEvent : Key → Event → Bool) (key : Key) - (xs : List α) - (hDistinct : List.Pairwise - (fun a b => ∀ ea eb, emit a = some ea → emit b = some eb → - ¬ (matchEvent key ea = true ∧ matchEvent key eb = true)) xs) : - countMatching matchEvent key (emitLoop emit xs) ≤ 1 := by - induction xs with - | nil => simp [emitLoop, countMatching] - | cons hd rest ih => - have hRestDistinct := (List.pairwise_cons.mp hDistinct).2 - have hHeadDistinct := (List.pairwise_cons.mp hDistinct).1 - simp only [emitLoop] - split - · rename_i e hEq - by_cases hHdMatch : matchEvent key e = true - · -- Head emits a matching event. No tail event can also match. - have hTailZero : - countMatching matchEvent key (emitLoop emit rest) = 0 := by - unfold countMatching - have : (emitLoop emit rest).filter (matchEvent key) = [] := by - apply List.filter_eq_nil_iff.mpr - intro e' he' hMatch' - obtain ⟨x', hx', hex'⟩ := emitLoop_event_origin emit rest e' he' - exact hHeadDistinct x' hx' e e' hEq hex' ⟨hHdMatch, hMatch'⟩ - rw [this]; simp - unfold countMatching at hTailZero ⊢ - simp only [List.filter_cons] - rw [if_pos hHdMatch] - simp [hTailZero] - · -- Head does not match. Recurse on tail. - unfold countMatching - simp only [List.filter_cons] - rw [if_neg (by simp [hHdMatch])] - exact ih hRestDistinct - · exact ih hRestDistinct - -end Verity.Trace diff --git a/artifacts/COMPILER_ARTIFACTS.md b/artifacts/COMPILER_ARTIFACTS.md deleted file mode 100644 index 774e5d4d3d..0000000000 --- a/artifacts/COMPILER_ARTIFACTS.md +++ /dev/null @@ -1,30 +0,0 @@ -# Compiler Artifacts - -This folder collects compiler artifacts and fixtures used by tests, CI, and documentation. - -## Contents - -- `yul/`: Generated Yul output used by Foundry tests and differential testing. Generated by `lake exe verity-compiler`. -- `macro_property_tests/`: Auto-generated Foundry property test stubs for all `verity_contract` contracts. Generated by `scripts/generate_macro_property_tests.py`. -- `verification_status.json`: Machine-readable verification metrics (theorem counts, axiom counts). Generated by `scripts/generate_verification_status.py`. -- `layer2_boundary_catalog.json`: Machine-readable Layer 2 theorem target and proof-boundary catalog. Generated by `scripts/generate_layer2_boundary_catalog.py`. -- `lean_warning_baseline.json`: Lean warning regression baseline. Consumed by `scripts/check_lean_warning_regression.py`. -- `interpreter_feature_matrix.json`: Machine-readable interpreter feature support matrix. -- `evmyullean_capability_report.json`: EVMYulLean builtin coverage report. Generated by `scripts/generate_evmyullean_capability_report.py`. -- `trust_surface_report.json`: Machine-readable trust-surface inventory (non-axiom trusted mechanisms, ECM assumption strings with source locations, Lean 4.31 kernel string-fact boundary). Generated by `scripts/generate_trust_surface_report.py`. -- `evmyullean_native_lowering_report.json`: EVMYulLean native lowering and bridge report. -- `evmyullean_unsupported_nodes.json`: EVMYulLean unsupported AST node report. -- `issue_1060_progress.json`: Issue #1060 migration tracking ledger (complete). - -## Related Source - -- Compiler implementation: `Compiler/` -- Proofs: `Compiler/Proofs/` -- Tests: `test/` (Foundry unit, property, and differential tests) - -## Assumptions - -Yul is compiled to bytecode using `solc` (trusted), and the runtime semantics are modeled in Lean. See: - -- Native EVMYulLean runtime proof boundary: `Compiler/Proofs/YulGeneration/Backends/EvmYulLeanNativeHarness.lean` -- Native lowering coverage: `Compiler/Proofs/YulGeneration/Backends/EvmYulLeanNativeLowering.lean` diff --git a/artifacts/STORAGE_LAYOUT_SUMMARY.md b/artifacts/STORAGE_LAYOUT_SUMMARY.md index 9d8e76bc3d..4386ebc971 100644 --- a/artifacts/STORAGE_LAYOUT_SUMMARY.md +++ b/artifacts/STORAGE_LAYOUT_SUMMARY.md @@ -5,7 +5,7 @@ _Auto-generated by `scripts/generate_storage_layout_report.py` from the generator after any change to a production storage layout._ Source of truth: `Compiler.Specs.allSpecs` (canonical production -contract surface). See `AUDIT.md` for the audit-artifact registry +contract surface). See `docs/TRUST_ASSUMPTIONS.md` for the audit-artifact registry and issue #1897 for the migration-review motivation. ## Non-alias certificate justifications (#1966) diff --git a/artifacts/feature_ownership.json b/artifacts/feature_ownership.json index 09f2ead19a..00181d5c6a 100644 --- a/artifacts/feature_ownership.json +++ b/artifacts/feature_ownership.json @@ -11,7 +11,7 @@ { "id": "source_internal_helpers", "name": "Source-level internal helper functions", - "owner": "Verity.Macro / Compiler.CompilationModel / Compiler.Proofs.SourceSemantics", + "owner": "Verity.Macro / Compiler.CompilationModel / Compiler.Proofs.IRGeneration.SourceSemantics", "source_supported": "supported", "compile_supported": "supported", "proof_modeled": "partial", @@ -20,7 +20,7 @@ "canonical_entrypoints": [ "Verity/Macro/Translate.lean", "Compiler/CompilationModel.lean", - "Compiler/Proofs/SourceSemantics.lean" + "Compiler/Proofs/IRGeneration/SourceSemantics.lean" ], "notes": "Internal helpers stay out of ABI dispatch. Helper-summary reuse is available in source-semantics lemmas but is not yet fully consumed through the generic body/IR theorem path." }, @@ -100,7 +100,7 @@ "deprecated": false, "canonical_entrypoints": [ "Verity/Macro/Translate.lean", - "Compiler/CompilationModel/StorageLayout.lean", + "Compiler/CompilationModel/LayoutValidation.lean", "docs/SOLIDITY_PARITY_PROFILE.md" ], "notes": "Many scalar, mapping, struct, packed-word, and ERC-7201 forms are present, but compatibility layout features are not all inside the generic proof fragment." @@ -121,25 +121,10 @@ ], "notes": "Zero-bound supported bodies and literal-bound empty bodies are covered. Positive non-empty loop bodies remain outside the current generic theorem." }, - { - "id": "legacy_spec_aliases", - "name": "Legacy CompilationModel spec aliases", - "owner": "Contracts.Legacy", - "source_supported": "not_applicable", - "compile_supported": "supported", - "proof_modeled": "not_applicable", - "trust_reported": "not_applicable", - "deprecated": true, - "canonical_entrypoints": [ - "Contracts/Legacy/SpecAliases.lean", - "Contracts/SpecAliases.lean" - ], - "notes": "Historical names remain available through a compatibility namespace. New code should import the canonical contract module and use Contracts..spec directly." - }, { "id": "patched_compiler_entrypoint", - "name": "Patch-enabled compiler binary", - "owner": "Compiler.MainPatched", + "name": "Patch-enabled compilation", + "owner": "Compiler.MainDriver", "source_supported": "not_applicable", "compile_supported": "supported", "proof_modeled": "not_applicable", @@ -147,11 +132,10 @@ "deprecated": false, "canonical_entrypoints": [ "Compiler/Main.lean", - "Compiler/MainPatched.lean", - "lakefile.lean", - "packages/verity-compiler/lakefile.lean" + "Compiler/MainDriver.lean", + "lakefile.lean" ], - "notes": "The canonical compiler binary is Compiler.Main / verity-compiler. MainPatched remains as the intentionally separate parity and patch-enabled binary." + "notes": "The Yul patch pass is enabled with `verity-compiler --enable-patches`; the former separate patched binary is folded into verity-compiler." }, { "id": "external_call_with_return_ecm_name", diff --git a/artifacts/issue_1060_progress.json b/artifacts/issue_1060_progress.json index 5487dc18da..302b5364d4 100644 --- a/artifacts/issue_1060_progress.json +++ b/artifacts/issue_1060_progress.json @@ -1924,8 +1924,8 @@ "AXIOMS.md and TRUST_ASSUMPTIONS.md accurately reflect zero-sorry state" ], "files_changed": [ - "AXIOMS.md", - "TRUST_ASSUMPTIONS.md" + "docs/AXIOMS.md", + "docs/TRUST_ASSUMPTIONS.md" ], "verification_commands": [ "lake build", diff --git a/artifacts/trust_surface_report.json b/artifacts/trust_surface_report.json index 5e79bc985d..c882da7e2d 100644 --- a/artifacts/trust_surface_report.json +++ b/artifacts/trust_surface_report.json @@ -167,10 +167,10 @@ ] }, "mechanisms": { - "@[implemented_by": 1, - "native_decide": 613, - "partial def": 183 + "@[implemented_by": 4, + "native_decide": 479, + "partial def": 194 }, - "notes": "native_decide trusts Lean.ofReduceBool or Lean 4.31 generated per-proof native_decide axioms + Lean.trustCompiler. Prose registry: AXIOMS.md, TRUST_ASSUMPTIONS.md (enforced by scripts/check_trust_surface_registry.py).", + "notes": "native_decide trusts Lean.ofReduceBool or Lean 4.31 generated per-proof native_decide axioms + Lean.trustCompiler. Prose registry: docs/AXIOMS.md, docs/TRUST_ASSUMPTIONS.md (enforced by scripts/check_trust_surface_registry.py).", "schema_version": 1 } diff --git a/artifacts/verification_status.json b/artifacts/verification_status.json index 0804e9dcac..8ef8e8ed64 100644 --- a/artifacts/verification_status.json +++ b/artifacts/verification_status.json @@ -1,7 +1,7 @@ { "codebase": { "core_lines": 2412, - "example_contracts": 20 + "example_contracts": 19 }, "proofs": { "axioms": 1, @@ -10,16 +10,16 @@ "schema_version": 1, "tests": { "differential_total": 110000, - "foundry_functions": 532, + "foundry_functions": 524, "property_functions": 239, - "suites": 53 + "suites": 52 }, "theorems": { - "categories": 17, - "coverage_percent": 72, + "categories": 15, + "coverage_percent": 78, "covered": 255, - "excluded": 101, - "non_stdlib_total": 356, + "excluded": 74, + "non_stdlib_total": 329, "per_contract": { "Counter": 31, "ERC20": 22, @@ -35,13 +35,11 @@ "SafeCounter": 25, "SimpleStorage": 20, "SimpleToken": 61, - "SolidityImportSmoke": 20, - "Vault": 9, - "VaultFromSolidity": 7 + "Vault": 9 }, - "proven": 356, + "proven": 329, "stdlib": 0, - "total": 356 + "total": 329 }, "toolchain": { "lean": "leanprover/lean4:v4.31.0", diff --git a/docs-site/artifacts/screenshots/dark-code-page.png b/docs-site/artifacts/screenshots/dark-code-page.png deleted file mode 100644 index 21263ceaf8..0000000000 Binary files a/docs-site/artifacts/screenshots/dark-code-page.png and /dev/null differ diff --git a/docs-site/artifacts/screenshots/docs-article-cream.png b/docs-site/artifacts/screenshots/docs-article-cream.png deleted file mode 100644 index 640777bfef..0000000000 Binary files a/docs-site/artifacts/screenshots/docs-article-cream.png and /dev/null differ diff --git a/docs-site/artifacts/screenshots/home-desktop-cream.png b/docs-site/artifacts/screenshots/home-desktop-cream.png deleted file mode 100644 index 79dd737968..0000000000 Binary files a/docs-site/artifacts/screenshots/home-desktop-cream.png and /dev/null differ diff --git a/docs-site/artifacts/screenshots/home-mobile-cream.png b/docs-site/artifacts/screenshots/home-mobile-cream.png deleted file mode 100644 index 535113f6d8..0000000000 Binary files a/docs-site/artifacts/screenshots/home-mobile-cream.png and /dev/null differ diff --git a/docs-site/content/capabilities.mdx b/docs-site/content/capabilities.mdx index fb9311bea8..03193b3187 100644 --- a/docs-site/content/capabilities.mdx +++ b/docs-site/content/capabilities.mdx @@ -59,7 +59,7 @@ Compiler-supported and differential-tested, but only partially covered by the pr These lower to intended Yul and are differential-tested, but their runtime semantics are **not fully modeled by the proof interpreters**. Treat each use as an explicit trust-boundary assumption and archive `--trust-report`. - **`keccak256`** — compiles through the typed intrinsic and uses the in-tree Keccak implementation on the executable source path, but the proof stack still treats dynamic memory hashing as an explicit primitive assumption (`keccak256_memory_slice_matches_evm`) rather than a fully modeled operation. Enforce exclusion with `--deny-axiomatized-primitives`. -- **ECMs, typed interface calls, external calls, and low-level mechanics** — `call` / `staticcall` / `delegatecall`, returndata plumbing, `externalCallBind`, typed Solidity interface dot-calls, linked Yul libraries such as Poseidon, CREATE2, SSTORE2-style `extcodecopy`, and ECMs (`callWithValue`, `bubblingValueCall`, no-output adapters/routers, SafeTransferLib-style optional-return helpers, callback helpers, CREATE2/SSTORE2 helpers, and the precompile ECMs `ecrecover` / `sha256` / BN254). Enforce with `--deny-low-level-mechanics` or `--deny-assumed-dependencies` depending on the surface; each precompile carries its own `evm_*_precompile` trust-report assumption, while CREATE2/SSTORE2 carry layout/address assumptions (see [`docs/EXTERNAL_CALL_MODULES.md`](https://github.com/lfglabs-dev/verity/blob/main/docs/EXTERNAL_CALL_MODULES.md) and [`AXIOMS.md`](https://github.com/lfglabs-dev/verity/blob/main/AXIOMS.md)). +- **ECMs, typed interface calls, external calls, and low-level mechanics** — `call` / `staticcall` / `delegatecall`, returndata plumbing, `externalCallBind`, typed Solidity interface dot-calls, linked Yul libraries such as Poseidon, CREATE2, SSTORE2-style `extcodecopy`, and ECMs (`callWithValue`, `bubblingValueCall`, no-output adapters/routers, SafeTransferLib-style optional-return helpers, callback helpers, CREATE2/SSTORE2 helpers, and the precompile ECMs `ecrecover` / `sha256` / BN254). Enforce with `--deny-low-level-mechanics` or `--deny-assumed-dependencies` depending on the surface; each precompile carries its own `evm_*_precompile` trust-report assumption, while CREATE2/SSTORE2 carry layout/address assumptions (see [`docs/EXTERNAL_CALL_MODULES.md`](https://github.com/lfglabs-dev/verity/blob/main/docs/EXTERNAL_CALL_MODULES.md) and [`AXIOMS.md`](https://github.com/lfglabs-dev/verity/blob/main/docs/AXIOMS.md)). - **`delegatecall` / proxy upgradeability** — additionally a dedicated proxy boundary. Enforce with `--deny-proxy-upgradeability` ([#1420](https://github.com/lfglabs-dev/verity/issues/1420)). - **Raw payload plumbing, local obligations, and proxy-specific refinements** — `calldatacopy`, `returndataCopy`, `revertReturndata`, `rawLog`, `local_obligations`, `unsafe "reason" do` blocks, and delegatecall/proxy refinements. Enforce raw-event exclusion with `--deny-event-emission`, local-obligation exclusion with `--deny-local-obligations`, unsafe-block exclusion with `--deny-unsafe`, and low-level mechanics with `--deny-low-level-mechanics`. - **Nested dynamic ABI decoding** — `arrayElementDynamicWord` / `paramDynamicHeadWord` read checked static leaf words from dynamic-shaped calldata; the decoded words are not modeled in proofs ([#1832](https://github.com/lfglabs-dev/verity/issues/1832)). diff --git a/docs-site/content/core.mdx b/docs-site/content/core.mdx index 1ad2a97a58..621c0d6acc 100644 --- a/docs-site/content/core.mdx +++ b/docs-site/content/core.mdx @@ -21,7 +21,7 @@ structure StorageSlot (α : Type) where `Uint256` is a dedicated 256-bit modular integer type, so `+`, `-`, `*`, `/`, and `%` wrap exactly like the EVM. -> **Note**: This matches EVM wrapping semantics, not Solidity's default checked arithmetic (^0.8). Use `require` guards to match Solidity behavior. See [TRUST_ASSUMPTIONS.md](https://github.com/lfglabs-dev/verity/blob/main/TRUST_ASSUMPTIONS.md#8-arithmetic-semantics-wrapping-vs-checked) for details. +> **Note**: This matches EVM wrapping semantics, not Solidity's default checked arithmetic (^0.8). Use `require` guards to match Solidity behavior. See [TRUST_ASSUMPTIONS.md](https://github.com/lfglabs-dev/verity/blob/main/docs/TRUST_ASSUMPTIONS.md#8-arithmetic-semantics-wrapping-vs-checked) for details. ## ContractState @@ -63,7 +63,7 @@ inductive ContractResult (α : Type) where Every contract operation returns either `success` with a value and updated state, or `revert` with an error message and the state at the point of failure. -> **Important**: On revert, the returned state may include mutations from operations that executed *before* the revert. This differs from EVM semantics where REVERT discards all state changes. Contracts must follow the **checks-before-effects pattern**, all `require` guards before any `setStorage`/`setMapping` calls, to ensure the revert state equals the original input state. See [TRUST_ASSUMPTIONS.md](https://github.com/lfglabs-dev/verity/blob/main/TRUST_ASSUMPTIONS.md) and [#254](https://github.com/lfglabs-dev/verity/issues/254) for details. +> **Important**: On revert, the returned state may include mutations from operations that executed *before* the revert. This differs from EVM semantics where REVERT discards all state changes. Contracts must follow the **checks-before-effects pattern**, all `require` guards before any `setStorage`/`setMapping` calls, to ensure the revert state equals the original input state. See [TRUST_ASSUMPTIONS.md](https://github.com/lfglabs-dev/verity/blob/main/docs/TRUST_ASSUMPTIONS.md) and [#254](https://github.com/lfglabs-dev/verity/issues/254) for details. ## Contract monad diff --git a/docs-site/content/faq.mdx b/docs-site/content/faq.mdx index 5c6aba73e9..d3e2de27b9 100644 --- a/docs-site/content/faq.mdx +++ b/docs-site/content/faq.mdx @@ -43,7 +43,7 @@ the supported fragment, the `IR → Yul` preservation, and per-contract specs and invariants. Trusted: the Lean kernel, the Yul compiler (`solc`), the EVM specification, the deployment toolchain, and the named external assumptions listed in `--trust-report` (ECM interface signatures, precompile semantics, -any `local_obligations`). [`AXIOMS.md`](https://github.com/lfglabs-dev/verity/blob/main/AXIOMS.md) +any `local_obligations`). [`AXIOMS.md`](https://github.com/lfglabs-dev/verity/blob/main/docs/AXIOMS.md) itself lists **zero active project-level Lean axioms** today, the residual ones have been eliminated. The full breakdown lives at [`/trust-model`](/trust-model). diff --git a/docs-site/content/guides/linking-libraries.mdx b/docs-site/content/guides/linking-libraries.mdx index e1668595c3..abcf78e39a 100644 --- a/docs-site/content/guides/linking-libraries.mdx +++ b/docs-site/content/guides/linking-libraries.mdx @@ -80,7 +80,7 @@ External libraries are **outside the formal verification boundary**. The linker In practice: a contract using `PoseidonT3_hash` via the linker has formally verified logic (storage, access control, arithmetic) but an *unverified* cryptographic core. The proof guarantees the contract correctly *uses* the hash; it does not guarantee the hash *is* Poseidon. Auditing the `.yul` library is the developer's responsibility. -To increase confidence: use audited implementations, add Foundry tests that exercise the linked contract end-to-end, compare library output against a reference, and document the assumption in [`TRUST_ASSUMPTIONS.md`](https://github.com/lfglabs-dev/verity/blob/main/TRUST_ASSUMPTIONS.md). +To increase confidence: use audited implementations, add Foundry tests that exercise the linked contract end-to-end, compare library output against a reference, and document the assumption in [`TRUST_ASSUMPTIONS.md`](https://github.com/lfglabs-dev/verity/blob/main/docs/TRUST_ASSUMPTIONS.md). ## CLI diff --git a/docs-site/content/reentrancy.mdx b/docs-site/content/reentrancy.mdx index 73dc6ac645..a1f2dd1ba8 100644 --- a/docs-site/content/reentrancy.mdx +++ b/docs-site/content/reentrancy.mdx @@ -159,7 +159,7 @@ reentry schedules with no new per-contract proof The framework is a proof *about a model*, and its trust boundary is recorded in -[`TRUST_ASSUMPTIONS.md`](https://github.com/lfglabs-dev/verity/blob/main/TRUST_ASSUMPTIONS.md): +[`TRUST_ASSUMPTIONS.md`](https://github.com/lfglabs-dev/verity/blob/main/docs/TRUST_ASSUMPTIONS.md): - **Registry completeness.** `entrypoints` must list *every* state-mutating public function. Omit one and the theorem quantifies over the wrong diff --git a/docs-site/content/trust-model.mdx b/docs-site/content/trust-model.mdx index a3386e85dc..f6f319be55 100644 --- a/docs-site/content/trust-model.mdx +++ b/docs-site/content/trust-model.mdx @@ -59,7 +59,7 @@ outside the proof envelope. ## Axioms and assumptions -[`AXIOMS.md`](https://github.com/lfglabs-dev/verity/blob/main/AXIOMS.md) is the authoritative registry of Lean axioms in the proof tree. Today it lists **zero active project-level axioms**: the last residual axiom (`solidityMappingSlot_lt_evmModulus`) was eliminated when the FFI-based `keccak256` call was replaced by the kernel-computable `KeccakEngine.keccak256` whose output length is proved by `squeeze256_size`. +[`AXIOMS.md`](https://github.com/lfglabs-dev/verity/blob/main/docs/AXIOMS.md) is the authoritative registry of Lean axioms in the proof tree. Today it lists **zero active project-level axioms**: the last residual axiom (`solidityMappingSlot_lt_evmModulus`) was eliminated when the FFI-based `keccak256` call was replaced by the kernel-computable `KeccakEngine.keccak256` whose output length is proved by `squeeze256_size`. What remains outside the proof envelope is **assumptions**, not axioms: diff --git a/docs-site/content/verification.mdx b/docs-site/content/verification.mdx index 7bf58115e7..3a49d981bd 100644 --- a/docs-site/content/verification.mdx +++ b/docs-site/content/verification.mdx @@ -5,7 +5,7 @@ description: Theorem inventory by contract, plus the compiler-proof structure. # Verification Status -All three pipeline layers (EDSL → CompilationModel → IR → Yul) are complete and machine-checked by `lake build`. Zero `sorry`. For *how* these proofs are built, see [Proof Techniques](/proof-techniques); for *what is trusted vs verified*, see [Trust Model](/trust-model). The authoritative axiom list lives in [AXIOMS.md](https://github.com/lfglabs-dev/verity/blob/main/AXIOMS.md). +All three pipeline layers (EDSL → CompilationModel → IR → Yul) are complete and machine-checked by `lake build`. Zero `sorry`. For *how* these proofs are built, see [Proof Techniques](/proof-techniques); for *what is trusted vs verified*, see [Trust Model](/trust-model). The authoritative axiom list lives in [AXIOMS.md](https://github.com/lfglabs-dev/verity/blob/main/docs/AXIOMS.md). ## Per-contract proof coverage diff --git a/docs-site/public/llms.txt b/docs-site/public/llms.txt index c8a39737ff..e2da1c067e 100644 --- a/docs-site/public/llms.txt +++ b/docs-site/public/llms.txt @@ -31,11 +31,11 @@ Every transition inside the proof envelope is either fully verified or recorded - **Language**: Lean 4.31.0 -- **Core Size**: 2257 lines -- **Verified Contracts**: 17 (Counter, ERC20, ERC721, Ledger, LocalObligationMacroSmoke, Ownable, Owned, OwnedCounter, OwnedCounterComposed, ReentrancyExample, ReentrancyRelyGuarantee, SafeCounter, SimpleStorage, SimpleToken, SolidityImportSmoke, Vault, VaultFromSolidity) -- **Theorems**: 356 across 17 categories, 356 fully proven -- **Axioms**: 1 documented Lean axioms (see AXIOMS.md) -- **Tests**: 532 Foundry tests, 239 property tests +- **Core Size**: 2412 lines +- **Verified Contracts**: 15 (Counter, ERC20, ERC721, Ledger, LocalObligationMacroSmoke, Ownable, Owned, OwnedCounter, OwnedCounterComposed, ReentrancyExample, ReentrancyRelyGuarantee, SafeCounter, SimpleStorage, SimpleToken, Vault) +- **Theorems**: 329 across 15 categories, 329 fully proven +- **Axioms**: 1 documented Lean axioms (see docs/AXIOMS.md) +- **Tests**: 524 Foundry tests, 239 property tests - **Build**: `lake build` verifies all proofs - **Repository**: https://github.com/lfglabs-dev/verity diff --git a/docs/ARCHITECTURE_PACKAGE_SPLIT.md b/docs/ARCHITECTURE_PACKAGE_SPLIT.md deleted file mode 100644 index a3a0ff9824..0000000000 --- a/docs/ARCHITECTURE_PACKAGE_SPLIT.md +++ /dev/null @@ -1,175 +0,0 @@ -# Architecture RFC: Split Verity into EDSL, Compiler, and Examples Packages - -Status: Proposed -Issue: #1307 - -## Objective - -Restructure the repository into strict package boundaries so canonical contract implementations are not mixed into framework internals. - -Target packages: - -1. `verity-edsl` -2. `verity-compiler` -3. `verity-examples` - -This RFC assumes a clean break with no compatibility aliases. - -## Why split this way - -- `verity-edsl` should define language semantics, macros, and verification interfaces. -- `verity-compiler` should consume EDSL artifacts and produce backend output without embedding concrete example contracts. -- `verity-examples` should own contracts, proofs, parity fixtures, and end-to-end test harnesses. - -This enforces one dependency direction: - -`verity-examples -> verity-compiler -> verity-edsl` - -No reverse imports are allowed. - -## Current package boundaries - -CI enforces import-direction checks and package-glob ownership checks on `main`. Current export surfaces: - -- `verity-edsl` exports only `Verity.*` modules. -- `verity-examples` exports only `Contracts.*` modules. -- `verity-compiler` exports `Compiler.*` plus a small `Verity.*` bridge surface (`Verity.Macro` and proof automation helpers) that still sits above compiler types. - -**Remaining work**: move the shared interface types out of `Compiler`, then shrink or eliminate the temporary `Verity.*` bridge surface in `verity-compiler`. - -## Package responsibilities - -## `verity-edsl` - -Owns: - -- `Verity/*` EDSL, semantics, macro implementation, proof utility infrastructure. - -Must not depend on: - -- `Compiler/*` -- Any concrete contract modules. - -Public API: - -- Stable imports for contract authors. -- Macro entrypoints used by examples. - -## `verity-compiler` - -Owns: - -- `Compiler/*` lowering, codegen, CLI, backend integration, proofs for compilation pipeline. - -Depends on: - -- `verity-edsl` - -Must not depend on: - -- Concrete contracts from examples. - -Design change required: - -- Replace internal global registry assumptions with explicit input manifest/module list provided at invocation time. - -## `verity-examples` - -Owns: - -- Canonical contracts and proofs. -- Solidity reference contracts used for differential testing. -- Foundry and differential test harnesses. - -Depends on: - -- `verity-edsl` -- `verity-compiler` - -Contract source policy: - -- One contract per canonical file/module. -- No mixed multi-contract buckets such as a monolithic `Core.lean` containing unrelated contracts. - -Suggested layout: - -- `examples/contracts/core/Counter.lean` -- `examples/contracts/core/Owned.lean` -- `examples/contracts/token/ERC20.lean` -- `examples/contracts/token/ERC721.lean` -- `examples/contracts/security/ReentrancyExample.lean` -- `examples/solidity-ref/*.sol` -- `examples/foundry/*` - -## Build and CLI implications - -Required compiler changes: - -1. Remove default baked-in "compile all known contracts" behavior from core internals. -2. Add explicit manifest-based selection: - - `--manifest ` for batch compilation. - - Optional `--module ` repeatable flag. -3. Keep deterministic output ordering based on manifest order. - -Manifest responsibility moves to `verity-examples`. - -## CI migration - -Replace monolithic coupling with package-scoped jobs plus integration: - -1. `edsl` job: - - Build/test only `verity-edsl`. -2. `compiler` job: - - Build/test `verity-compiler` against published/local `verity-edsl`. -3. `examples` job: - - Build examples/proofs against pinned `verity-edsl` + `verity-compiler`. -4. `parity` job: - - Generate Yul via compiler and run Foundry differential tests using `examples/solidity-ref`. -5. `integration` job: - - End-to-end smoke across all three packages. - -Also add import-boundary linting per package to fail reverse dependencies. - -## Migration plan (no compatibility aliases) - -Phase 1: Introduce package skeletons and dependency boundaries. - -- Create package directories/workspace config. -- Move files without semantic edits. -- Keep all checks green. - -Phase 2: Decouple compiler from contract registry. - -- Introduce manifest/module-input compilation path. -- Remove direct contract imports from compiler binaries/tests. - -Phase 3: Move examples and tests. - -- Relocate contracts/proofs/foundry/parity fixtures into `verity-examples`. -- Split macro contract definitions to one contract per module. - -Phase 4: Purge legacy coupling. - -- Remove old mixed-location modules. -- Remove any scripts that assume single-package roots. -- Enforce boundary checks in CI. - -## Risks and mitigations - -Risk: Build and cache churn due to package graph changes. -Mitigation: Introduce a root workspace orchestrator and cache keys per package. - -Risk: Script breakage from hardcoded paths. -Mitigation: Add package-aware path discovery and migrate scripts in the same PR series. - -Risk: Temporary contributor confusion during transition. -Mitigation: Add one architecture page per package and a root migration guide. - -## Acceptance criteria - -1. `verity-edsl` builds without importing compiler/examples modules. -2. `verity-compiler` builds without importing concrete contracts. -3. `verity-examples` builds against pinned versions of the two lower layers. -4. Differential tests consume `examples/solidity-ref` fixtures. -5. New contracts are added one contract per module, no multi-contract bucket modules. -6. CI enforces the dependency direction. diff --git a/docs/ARITHMETIC_PROFILE.md b/docs/ARITHMETIC_PROFILE.md index f120bb67dc..f3d9db3db2 100644 --- a/docs/ARITHMETIC_PROFILE.md +++ b/docs/ARITHMETIC_PROFILE.md @@ -254,7 +254,7 @@ The arithmetic model is invariant across profiles. See [`docs/SOLIDITY_PARITY_PR the guard/panic/arithmetic sequence) to a checked helper call is protected by structural matching and regression tests, not an end-to-end preservation theorem. -- **Cryptographic primitives**: keccak256 is axiomatized (see [`AXIOMS.md`](../AXIOMS.md)). +- **Cryptographic primitives**: keccak256 is axiomatized (see [`AXIOMS.md`](AXIOMS.md)). - **Universal bridge equivalence**: 25/25 pure EVMYulLean-backed builtins have universal bridge lemmas. All 25 also have context-lifted native bridge theorems. All 8 higher-level expression operators also have proven compilation correctness. ## Auditor Checklist @@ -271,7 +271,7 @@ The arithmetic model is invariant across profiles. See [`docs/SOLIDITY_PARITY_PR ## Related Documents -- [`TRUST_ASSUMPTIONS.md`](../TRUST_ASSUMPTIONS.md): trust boundaries and semantic caveats -- [`AXIOMS.md`](../AXIOMS.md): documented axioms (arithmetic is NOT an axiom) +- [`TRUST_ASSUMPTIONS.md`](TRUST_ASSUMPTIONS.md): trust boundaries and semantic caveats +- [`AXIOMS.md`](AXIOMS.md): documented axioms (arithmetic is NOT an axiom) - [`docs/SOLIDITY_PARITY_PROFILE.md`](SOLIDITY_PARITY_PROFILE.md): backend profile specification - [`Compiler/Proofs/ArithmeticProfile.lean`](../Compiler/Proofs/ArithmeticProfile.lean): formal proofs diff --git a/AXIOMS.md b/docs/AXIOMS.md similarity index 94% rename from AXIOMS.md rename to docs/AXIOMS.md index b0007d06fe..d2bc6fb202 100644 --- a/AXIOMS.md +++ b/docs/AXIOMS.md @@ -2,24 +2,14 @@ This file is the authoritative registry of axioms used by Verity proof code. -## Proof-only Solidity Vault audit - -`PrintAxioms.lean` includes the imported Vault execution theorems. The focused -`solidity_importer_test.py` runs `#print axioms` in a disposable audit module for -every theorem in `Contracts/VaultFromSolidity/Proofs/ExecutionProof.lean` and requires -coverage of all declared theorems, rejecting `sorryAx` and project axioms. -Its malformed-declaration probe also checks that kernel error recovery does not -leave any partial declarations or fallback axioms in the import namespace. -The imported Vault proofs report only the standard Lean foundations `propext` -and `Quot.sound`; they do not depend on `solidityMappingSlot_injective`. -`solvent_invariant` is included in that audit. The inheritance-slice suite -`inheritance_test.py` runs the same `#print axioms` gate on every theorem in -`Contracts/SolidityImportSmoke/Inheritance/Proofs.lean` and likewise requires -only `propext` and `Quot.sound`. The modifiers and structs suites -`modifiers_test.py` and `structs_test.py` run the same gate on -`Contracts/SolidityImportSmoke/Modifiers/Proofs.lean` and -`Contracts/SolidityImportSmoke/Structs/Proofs.lean`. This does not remove the trusted Solidity frontend/translation boundary described -in `TRUST_ASSUMPTIONS.md`, or change the compiler axiom registry below. +## Solidity import + +`smoke.covered` and `smoke_step_credit` are ordinary theorems. The import +path adds no project axiom, `sorry`, or `native_decide`. `covered` is +`decide` on the whitelist. `smoke_step_credit` is `simp` plus `decide` on the +concrete packed read. Interpreter `#eval` checks are not theorems. The frontend that +builds the model from solc JSON remains a trust assumption; see +`TRUST_ASSUMPTIONS.md`. ## Policy @@ -185,7 +175,7 @@ proof work (issue #1618). `supported_function_correct` is now a real theorem. ### 4. Layer 3 dispatch bridge (eliminated earlier) The dispatch bridge in Layer 3 was converted from a Lean axiom to an explicit -theorem hypothesis in `Compiler/Proofs/YulGeneration/Preservation.lean`. +theorem hypothesis (in the former `Compiler/Proofs/YulGeneration/Preservation.lean`, since removed). ## Trusted Cryptographic Primitives (Non-Axiom) @@ -388,7 +378,7 @@ with no remaining pure builtins relying only on concrete bridge checks. Additionally, 8 higher-level expression operators have proven compilation correctness in the `ExprCompileCore` fragment: `min`, `max`, `ceilDiv`, `ite` (conditional), `wMulDown`, `wDivUp`, `mulDivDown`, and `mulDivUp`. See -[`docs/ARITHMETIC_PROFILE.md`](docs/ARITHMETIC_PROFILE.md) for the full +[`docs/ARITHMETIC_PROFILE.md`](ARITHMETIC_PROFILE.md) for the full specification. The structured checked-arithmetic panic migration adds **no project-level @@ -470,8 +460,7 @@ proof exists. Any commit that adds, removes, renames, or moves a project-level axiom must update this file in the same commit. Any commit that changes intrinsic trust -semantics must update this file, [TRUST_ASSUMPTIONS.md](TRUST_ASSUMPTIONS.md), -and [AUDIT.md](AUDIT.md). +semantics must update this file and [TRUST_ASSUMPTIONS.md](TRUST_ASSUMPTIONS.md). If this file is stale, trust analysis is stale. diff --git a/docs/C2_STORAGE_LENS_PART2_SCOPE.md b/docs/C2_STORAGE_LENS_PART2_SCOPE.md deleted file mode 100644 index d241f4b0c6..0000000000 --- a/docs/C2_STORAGE_LENS_PART2_SCOPE.md +++ /dev/null @@ -1,72 +0,0 @@ -# C2 Storage Lens Part 2 Scope - -## Current State - -C1 and C2 part 1 are already landed, but this checkout has no committed -`storage_lens`, `StorageLens`, or "storage lens" symbols under `Compiler/`, -`Contracts/`, or `Compiler/Proofs/`. The C2 part 2 slice therefore needs a -small proof-facing entry point before it can safely refactor compiler or layout -surfaces. - -## Smallest Landable Part 2 Unit - -Pick one new lens type: `view_lens`. - -This is the smallest useful unit because a view lens can be introduced as a -read-only projection over existing flat-storage and mapping-slot proof -surfaces, without changing compiler lowering or taking ownership of the -uncommitted transient layout work. - -## Proposed Shape - -The first implementation should live near the proof-facing storage model, most -likely under `Compiler/Proofs/StorageLens.lean`, imported only by a focused test -or proof file until the API is stable. - -Minimal API: - -```lean -structure ViewLens (α : Type) where - read : (IRStorageSlot → IRStorageWord) → α -``` - -First concrete constructors: - -- `ViewLens.word slot`: reads one resolved storage slot. -- `ViewLens.mappingWord baseSlot key`: reads through - `solidityMappingSlot baseSlot key`. - -The mapping constructor should reuse `Compiler.Proofs.MappingSlot` rather than -introducing a second slot-derivation model. - -## First Proof/Test - -Land one theorem proving that a write to a different flat slot preserves a -`ViewLens.word` read: - -```lean -theorem ViewLens.word_read_store_other - (storage : IRStorageSlot → IRStorageWord) - (readSlot writeSlot value : Nat) - (h : readSlot ≠ writeSlot) : - (ViewLens.word readSlot).read - (abstractStoreStorageOrMapping storage writeSlot value) = - (ViewLens.word readSlot).read storage -``` - -This theorem is narrow, builds on `abstractStoreStorageOrMapping`, and gives C2 -part 2 a concrete proof artifact before broadening to mapping/non-alias -certificates. - -## Not In This Unit - -- No compiler lowering changes. -- No layout report changes. -- No transient mapping semantics yet. -- No global storage-lens import into Layer 2 or native Layer 3 proofs. - -## Follow-Up Slice - -After `ViewLens.word` lands, add `ViewLens.mappingWord` preservation using the -finite non-alias certificate surface already present in -`Compiler/Proofs/MappingSlot.lean`. diff --git a/docs/CI_RUNNERS.md b/docs/CI_RUNNERS.md index 5a471e1709..35b9e50e81 100644 --- a/docs/CI_RUNNERS.md +++ b/docs/CI_RUNNERS.md @@ -157,9 +157,6 @@ This runs `make check`, full `lake build`, PrintAxioms/audit checks, and compiler-core validation. On ARM64 it skips x64-only `solc` validation and runs portable generated-Yul checks instead. -The latest benchmark report is in -[`docs/CI_RUNNER_BENCHMARK_REPORT.md`](CI_RUNNER_BENCHMARK_REPORT.md). - ## Maintenance Expectations Each CI host should have the daily Verity maintenance timer installed. It diff --git a/docs/CI_RUNNER_BENCHMARK_REPORT.md b/docs/CI_RUNNER_BENCHMARK_REPORT.md deleted file mode 100644 index 4adf71c120..0000000000 --- a/docs/CI_RUNNER_BENCHMARK_REPORT.md +++ /dev/null @@ -1,260 +0,0 @@ -# Verity Self-Hosted Runner Validation Report - -Date: 2026-05-05 - -This report compares the connected self-hosted runners with real Verity CI -work. The validation was run through GitHub Actions, with each job explicitly -forced onto a selected runner label set. - -For the hardware-level explanation of why the DGX is faster, which Verity tasks -are CPU-bound, and what hardware is likely to beat it, see -[`docs/CI_RUNNER_HARDWARE_ANALYSIS.md`](CI_RUNNER_HARDWARE_ANALYSIS.md). - -## Current Runner Architecture - -```text -lfglabs-dev/verity GitHub Actions - | - |-- build-88-99-4-254-1 - | host: 88.99.4.254 - | architecture: Linux X64 - | role: default heavy x64 Lean/build/proof jobs - | labels: self-hosted, Linux, X64, verity, build, cpu-8, mem-64g, - | build-heavy, ci-host-88-99-4-254, hetzner, hz2 - | - |-- tmd-verity-fastlane-1 - | host: 95.216.244.60 - | architecture: Linux X64 - | role: standby x64 fastlane fallback - | labels: self-hosted, Linux, X64, verity, fastlane, cpu-8, - | ci-host-95-216-244-60, hetzner, hz1 - | - |-- dgx-spark-gpu-1 - host: spark-de79 over Tailscale - architecture: Linux ARM64 - role: primary fastlane, trusted GPU jobs, explicit ARM64 Lean validation - labels: self-hosted, Linux, ARM64, verity, fastlane, dgx, dgx-spark, - gpu, nvidia, home, arm64-gb10 -``` - -Production workflow routing remains capability based: - -```yaml -runs-on: [self-hosted, linux, ARM64, dgx-spark, verity, fastlane] -runs-on: [self-hosted, linux, x64, verity, build] -runs-on: [self-hosted, linux, ARM64, dgx-spark, gpu] -``` - -The DGX intentionally has `verity,fastlane` but not `build`. Ordinary short -fastlane jobs land there, while full proof/build jobs stay on the x64 build -runner. - -## Validation Workflow - -The repository now includes a manual-only validation workflow: - -```text -.github/workflows/runner-lean-validation.yml -``` - -It accepts a JSON `runs-on` label list and runs a CI-equivalent Lean/proof -validation on that runner: - -- `make check` -- full `lake build` -- Lean warning regression check -- `lake build PrintAxioms` -- split package, macro fuzz coverage, axiom, proof length, property coverage, - and storage layout audits -- optional compiler-core validation: - - build `verity-compiler` - - build `verity-compiler-patched` - - build `difftest-interpreter` - - build `gas-report` - - generate baseline and patched Yul - - generate static gas reports - - on x64, install a pinned job-local `solc` and run `check_yul.py` - - on ARM64, run filename parity and EVM/Yul builtin boundary checks when - `solc` is unavailable - -The workflow is intentionally `workflow_dispatch` only. It is for validating -runner capability, benchmarking new hosts, and qualifying architecture changes. - -## Host Inventory - -| Runner | Host | CPU | RAM | Root disk free | Notes | -| --- | --- | --- | --- | --- | --- | -| `build-88-99-4-254-1` | `Ubuntu-2404-noble-amd64-base` | Intel i7-6700, 4C/8T | 62 GiB | about 680 GiB free | Healthy x64 build host. | -| `tmd-verity-fastlane-1` | `tmd` | Intel i7-7700, 4C/8T | 62 GiB | about 95 GiB free | Standby x64 fastlane fallback; still disk constrained. | -| `dgx-spark-gpu-1` | `spark-de79` | 20 ARM64 cores: Cortex-X925 + Cortex-A725 | 119 GiB | about 1.6 TiB free | Primary fastlane and explicit ARM64/GPU runner. | - -## Cold-ish Validation Results - -These are the most useful comparison numbers because they include substantial -real work rather than only warm cache no-ops. - -| Step | DGX ARM64 | Hz2 build x64 | Hz1 fastlane x64 | -| --- | ---: | ---: | ---: | -| `make check` | 34s | 75s | 82s | -| `lake build` | 111s | 279s | 275s | -| `lake build PrintAxioms` | 231s | 588s | 572s | -| `PrintAxioms report` | 81s | 171s | 175s | -| `build verity-compiler` | 31s | 142s | not run | -| `build verity-compiler-patched` | 3s | 8s | not run | -| `build difftest-interpreter` | 101s | 402s | not run | -| `build gas-report` | 2s | 4s | not run | - -Relevant runs: - -| Runner | Run | Result | Notes | -| --- | --- | --- | --- | -| DGX ARM64 | `25387433137` | failed at final `solc` check | Lean and compiler-core work passed before the x64-only `solc` assumption. | -| DGX ARM64 | `25388394404` | success | ARM64-aware validation passed. | -| Hz2 build x64 | `25387433160` | failed at final `solc` check | Lean and compiler-core work passed before the runner-local `solc` installer was fixed. | -| Hz2 build x64 | `25389590164` | success | x64 validation passed with pinned job-local `solc`. | -| Hz1 fastlane x64 | `25387433172` | success | Lean/audit-only validation passed with compiler-core disabled. | - -## Warm Validation Results - -After caches were warm, both DGX and Hz2 completed the full validation -successfully: - -| Runner | Run | Job wall clock | Notable timings | -| --- | --- | ---: | --- | -| DGX ARM64 | `25388394404` | 2m18s | `make check` 35s, `PrintAxioms report` 81s, compiler-core rebuilds 0-1s. | -| Hz2 build x64 | `25389590164` | 4m45s | `make check` 80s, `PrintAxioms report` 173s, compiler-core rebuilds 0-1s. | - -## Findings - -The DGX is materially faster for the real Lean and compiler-core tasks tested. -On the cold-ish validation run it was roughly: - -- 2.2x faster for `make check`; -- 2.5x faster for full `lake build`; -- 2.5x faster for `lake build PrintAxioms`; -- 2.1x faster for the PrintAxioms report; -- 4.6x faster for the first `verity-compiler` build; -- 4.0x faster for the first `difftest-interpreter` build. - -The DGX can run the Lean/proof-heavy part of Verity CI reliably on ARM64. It -also built the Verity compiler-core executables on ARM64 and generated Yul and -gas reports. - -The DGX should still not become the default general Verity build runner yet. The -remaining x64-specific surface is real: - -- production Solidity/Yul checking currently expects a Linux x64 `solc`; -- Foundry and other EVM tooling should be audited before default ARM64 use; -- generated compiler artifacts may be consumed by x64 assumptions elsewhere; -- the DGX is also a valuable GPU/inference host and should not receive - accidental full-build CPU-only queue pressure; -- the DGX is reachable through private networking and should stay in a - restricted runner group with trusted workflows only. - -The x64 build host remains necessary as the default production proof runner for -now. The old x64 fastlane host remains useful as a fallback and for -host-pinned debugging, but primary fastlane routing now points at the DGX. - -## Reliability Fixes Made During Validation - -Cancelled runner jobs previously left child `lake` or interpreter processes -alive because GitHub's generated service used: - -```ini -KillMode=process -``` - -All three hosts now have a systemd drop-in: - -```ini -[Service] -KillMode=control-group -SendSIGKILL=yes -TimeoutStopSec=90s -``` - -The repository runner installer writes the same drop-in for future installs. - -The validation workflow also found that `solc` setup should avoid host-global -installation and sudo assumptions. The final workflow installs a pinned `solc` -into `$RUNNER_TEMP/bin` and appends that directory to `GITHUB_PATH`, which -works on the non-root runner services. - -## Recommended Architecture Change - -Promote the DGX to primary fastlane and keep it as a first-class explicit -ARM64 Lean lane, but do not move all CI to it. - -Recommended steady state: - -```text -lfglabs-dev/verity - | - |-- hz1 fastlane - | labels: self-hosted, linux, x64, verity, fastlane - | use: standby fallback and temporary host-pinned debugging - | - |-- hz2 x64 build - | labels: self-hosted, linux, x64, verity, build - | use: default full proof/build CI and x64 external-tool validation - | - |-- DGX ARM64 Lean/GPU - labels: self-hosted, linux, ARM64, dgx-spark, verity, fastlane, gpu - optional future label: dgx-lean or verity-arm64 - use: primary fastlane, trusted/manual/scheduled ARM64 Lean proof - validation, and GPU jobs -``` - -Concrete next repo change to consider: - -- Keep production fastlane jobs routed to - `[self-hosted, linux, ARM64, dgx-spark, verity, fastlane]`. -- Add a scheduled or manually triggered `DGX Lean validation` job that reuses - the validation task set and routes to - `[self-hosted, linux, ARM64, dgx-spark, gpu]`. -- Keep it separate from required PR checks until it has passed repeatedly and - the x64 external-tool assumptions have been removed or split by architecture. -- If the label list becomes hard to read, add a new runner label such as - `dgx-lean` or `verity-arm64` and route only explicit DGX Lean jobs to it. - -## Adding New Servers - -For a new CPU server: - -1. Install the official GitHub runner as a systemd service with this repo's - `scripts/install_self_hosted_runner.sh`. -2. Start with one build runner per 8-core host. -3. Add precise labels such as `verity,build,cpu-16,mem-128g,`. -4. Run `runner-lean-validation.yml` against the proposed label set. -5. Compare `make check`, `lake build`, `lake build PrintAxioms`, and - `PrintAxioms report` before adding the host to production routing. - -Do not add several full build runners to the same host unless per-job -`LEAN_NUM_THREADS` is lowered and measured. Verity's Lean jobs are CPU and cache -intensive; oversubscription makes reliability and tail latency worse. - -## Minimal Context For Future Agents - -```md -Verity uses official GitHub Actions self-hosted runners installed as systemd -services. Route by labels and capability, not by hostname. - -Current production: -- 88.99.4.254: one x64 build/proof runner, labels `verity,build,hetzner,hz2` -- 95.216.244.60: one x64 standby fastlane runner, labels - `verity,fastlane,hetzner,hz1` -- spark-de79 DGX: one ARM64 primary fastlane/GPU/Lean validation runner, - labels `verity,fastlane,dgx,dgx-spark,gpu,nvidia` - -Rules: -- Keep fastlane and full proof/build jobs on separate labels. -- Do not run two full `LEAN_NUM_THREADS=8` Verity builds on one 4C/8T host. -- Add build capacity by adding machines before adding same-host runner count. -- DGX runs normal fastlane PR CI only. Use explicit `dgx-spark,gpu` or future - `dgx-lean` / `verity-arm64` labels for heavier trusted ARM64/GPU jobs. -- Keep x64 build CI for `solc`, Foundry, and x64 artifact assumptions until - those paths are split or proven portable. -- Keep runner systemd services hardened with `KillMode=control-group`. -- Maintain `/srv/verity-ci-cache`; sticky caches are useful but must be pruned. -- Use `.github/workflows/runner-lean-validation.yml` to qualify new machines. -``` diff --git a/docs/CI_RUNNER_HARDWARE_ANALYSIS.md b/docs/CI_RUNNER_HARDWARE_ANALYSIS.md deleted file mode 100644 index 8f29aa2ff5..0000000000 --- a/docs/CI_RUNNER_HARDWARE_ANALYSIS.md +++ /dev/null @@ -1,311 +0,0 @@ -# Verity Runner Hardware Analysis - -Date: 2026-05-06 - -This note explains why the DGX Spark is much faster than the two current -Hetzner x64 runners on real Verity CI work, what the slow CI tasks actually do, -where GPU acceleration can help, and which hardware classes are likely to beat -the current DGX result. - -Baseline timings come from -[`docs/CI_RUNNER_BENCHMARK_REPORT.md`](CI_RUNNER_BENCHMARK_REPORT.md). - -## Current Hardware - -Live host inspection and vendor specifications line up as follows: - -| Runner | Host | CPU | Cores exposed to Linux | Max CPU clock | Cache | Memory | Disk state | -| --- | --- | --- | ---: | ---: | --- | --- | --- | -| `dgx-spark-gpu-1` | `spark-de79` | NVIDIA GB10 / Grace Blackwell Arm CPU: 10 Cortex-X925 + 10 Cortex-A725 | 20 physical cores, no SMT | X925 up to 4.004 GHz, A725 up to 2.860 GHz | 25 MiB aggregate L2, 24 MiB L3 | 119 GiB available to Linux; 128 GiB LPDDR5x unified, 273 GB/s vendor bandwidth | 3.7 TiB root, about 1.6 TiB free | -| `build-88-99-4-254-1` | `88.99.4.254` | Intel Core i7-6700, Skylake | 4 cores / 8 SMT threads | up to 4.0 GHz | 1 MiB aggregate L2, 8 MiB L3 | 62 GiB DDR4/DDR3L-class, 34.1 GB/s vendor max bandwidth | 873 GiB root, about 699 GiB free | -| `tmd-verity-fastlane-1` | `95.216.244.60` | Intel Core i7-7700, Kaby Lake | 4 cores / 8 SMT threads | up to 4.2 GHz | 1 MiB aggregate L2, 8 MiB L3 | 62 GiB DDR4/DDR3L-class, dual-channel | 1.4 TiB root, about 132 GiB free | - -The most important comparison is not clock speed. It is: - -- DGX has 20 real CPU cores, while each Hetzner host has 4 physical cores plus - SMT. -- DGX has much more memory bandwidth: NVIDIA publishes 273 GB/s for the Spark, - while the i7-6700 class is 34.1 GB/s max memory bandwidth and the i7-7700 is - similar dual-channel DDR4 generation hardware. -- DGX has newer high-performance CPU cores and much larger per-job headroom. -- DGX has a much larger, healthier local disk and cache area. - -## Measured Verity Speedup - -Cold-ish validation measurements: - -| Step | DGX ARM64 | Hz2 i7-6700 | Hz1 i7-7700 | DGX vs best x64 | -| --- | ---: | ---: | ---: | ---: | -| `make check` | 34s | 75s | 82s | 2.2x | -| `lake build` | 111s | 279s | 275s | 2.5x | -| `lake build PrintAxioms` | 231s | 588s | 572s | 2.5x | -| `PrintAxioms report` | 81s | 171s | 175s | 2.1x | -| `build verity-compiler` | 31s | 142s | not run | 4.6x | -| `build difftest-interpreter` | 101s | 402s | not run | 4.0x | - -The DGX is faster on the CPU-bound Lean and compiler-core tasks. These numbers -do not require GPU acceleration to explain them. - -## Why The DGX Is Faster - -### More Real CPU Parallelism - -Verity's full proof/build jobs use `LEAN_NUM_THREADS=8`. On the Intel hosts, -that asks an 8-thread Lean job to run on a 4-core CPU with SMT. SMT is useful, -but two hardware threads on one core do not behave like two independent cores -for proof elaboration, hashing, allocation, parsing, code generation, and -artifact writes. - -On the DGX, an 8-thread Lean job has 20 physical Arm cores available. The job -can use eight real cores while the OS, GitHub runner, Lake, Python checks, and -I/O helpers still have spare cores. That directly improves both average runtime -and tail latency. - -### Memory Bandwidth And Cache Pressure - -Lean proof builds allocate and traverse a lot of structured data: syntax trees, -environments, expressions, constraints, theorem metadata, imported module -state, and generated `.olean`/native build artifacts. This is not a tiny -compute kernel that lives entirely in L1. - -The DGX Spark's LPDDR5x system memory is published at 273 GB/s. The i7-6700 is -published at 34.1 GB/s max memory bandwidth. Even allowing for real-world -differences, the DGX has a much wider memory system for the kind of -multi-threaded compiler workload Verity runs. - -The DGX also has materially more CPU cache than the Intel hosts: live `lscpu` -reports 25 MiB aggregate L2 and 24 MiB L3, versus 1 MiB aggregate L2 and 8 MiB -L3 on each Intel host. - -### Newer Cores - -The Intel machines are 2015-2017 desktop CPUs. The i7-6700 is Skylake, and the -i7-7700 is Kaby Lake. The DGX Spark CPU complex is current Arm high-performance -silicon built into NVIDIA GB10. For Lean, the relevant win is modern -single-thread throughput combined with enough physical cores to avoid -oversubscription. - -### Healthier Storage And Caches - -Verity benefits from sticky Lake and compiler caches, but stale cache pressure -can hurt both speed and reliability. The DGX root disk is large and currently -has about 1.6 TiB free. The standby x64 fastlane host is still much tighter at -about 132 GiB free and has historically needed cleanup. - -This is not the main reason `lake build` is 2.5x faster, but it helps prevent -cache churn and reduces the chance that a run slows down because the host is -fighting disk pressure. - -## What Actually Takes Time In Verity CI - -### Fastlane - -The fastlane jobs mostly run Python and repository consistency checks: - -- workflow/spec synchronization; -- generated documentation checks; -- property manifest and coverage checks; -- path, package-boundary, storage-layout, and macro-health checks; -- Python unit tests. - -These are mostly CPU-light, filesystem-heavy, and process-startup-heavy. They -benefit from the DGX because it has fast cores, idle headroom, and healthy disk, -but they do not need a GPU. - -### Full `lake build` - -This is the main proof/type-checking workload. It builds about 276 Lean source -files in this worktree, including contracts, compiler modules, proof modules, -and supporting packages. - -Most of the time is spent in Lean frontend/elaboration/type-checking/proof -checking work and writing build artifacts. For proof-heavy modules, "compile" -does not just mean machine-code generation. It means checking definitions, -theorems, tactics, imports, generated terms, and module dependencies so Lean can -emit trusted artifacts. - -This task benefits from: - -- strong per-core CPU performance; -- enough physical cores for `LEAN_NUM_THREADS`; -- memory bandwidth; -- fast local caches and storage; -- avoiding two large Lean builds on the same small CPU. - -It does not naturally benefit from the DGX GPU today. - -### `lake build PrintAxioms` And `PrintAxioms report` - -This is also Lean-heavy. Building `PrintAxioms` prepares the Lean executable or -environment needed for the axiom audit. Running the report walks theorem and -dependency information and prints/checks axiom usage. - -It is CPU and memory intensive. It can parallelize partly through Lean/Lake -builds, but the report generation itself has serial sections and dependency -traversal. GPU acceleration is not a natural fit unless this tooling is -redesigned around a GPU-friendly batch kernel, which is unlikely to be worth it. - -### Compiler-Core Binaries - -Targets such as `verity-compiler`, `verity-compiler-patched`, -`difftest-interpreter`, and `gas-report` include Lean build work and native -binary generation/linking. The DGX result was 4x or better on the first builds, -which suggests the current x64 hosts are bottlenecked by physical cores, memory -bandwidth, and native compile/link throughput. - -GPU acceleration does not help the normal Lean/C++ build path. A faster CPU, -faster linker/toolchain setup, and better ccache behavior help. - -### Foundry, `solc`, And EVM Differential Work - -These jobs are still x64-production-oriented. They include external tools, -generated Yul, Solidity compiler checks, Foundry tests, and interpreter runs. -Some of that work is serial, some is test-parallel, and much of it assumes x64 -Linux binaries today. - -Do not route these to DGX by default until ARM64 tool availability and parity -are fully boring. - -## What Could Use The GPU - -The DGX GPU is valuable, but not for normal Lean proof checking today. - -Good GPU candidates: - -- ML workloads, inference, model evaluation, and fine-tuning; -- LLM-assisted proof search experiments; -- neural premise selection, theorem ranking, or tactic suggestion; -- large embedding/indexing jobs over theorem/proof/code corpora; -- fuzzing or search loops where a learned model guides candidate generation; -- GPU-native benchmarking unrelated to the standard CI proof check. - -Poor GPU candidates: - -- `lake build`; -- Lean elaboration/type checking; -- `PrintAxioms`; -- Python sync checks; -- C++/Lean compiler binary builds; -- `solc` and Foundry validation. - -The practical split should be: keep standard correctness CI CPU-driven; use the -DGX GPU for optional or explicit AI/proof-assistant experiments. - -## Hardware That Could Be Faster - -The DGX is dramatically faster than the current 4-core Intel hosts, but it is -not the ceiling for Lean CI. The best next CPU runner is probably a modern -high-clock x86 workstation/server, not another small GPU appliance. - -### Best Practical Next CPU Host - -A modern 16-core Zen 5 host, for example AMD Ryzen 9 9950X-class hardware, is a -strong candidate: - -- 16 cores / 32 threads; -- up to 5.7 GHz boost; -- 4.3 GHz base; -- 64 MiB L3; -- AVX512 support; -- commodity x64 Linux compatibility. - -Expected outcome: likely faster than DGX for many x64 Lean and compiler-core -jobs if cooling and storage are good, because it combines high single-thread -performance, enough real cores for `LEAN_NUM_THREADS=8`, and native x64 tooling. -It would also remove the ARM64/x64 split for `solc`, Foundry, and binary -artifact assumptions. - -### Best Workstation-Class Build Host - -AMD Ryzen Threadripper 9970X-class hardware is more expensive but attractive -for many concurrent proof/build lanes: - -- 32 cores / 64 threads; -- up to 5.4 GHz boost; -- 4.0 GHz base; -- 128 MiB L3; -- 4-channel DDR5 RDIMM; -- many PCIe lanes for NVMe cache disks. - -Expected outcome: probably faster than DGX for full Verity CI and much better -for running multiple isolated build lanes, as long as each lane still has sane -`LEAN_NUM_THREADS` limits. - -### Best Server-Class Host - -AMD EPYC 9575F-class hardware is the high-end server option: - -- 64 cores / 128 threads; -- up to 5.0 GHz boost; -- 4.5 GHz all-core boost; -- 256 MiB L3; -- 12-channel DDR5 with 614 GB/s per-socket published bandwidth. - -Expected outcome: likely faster than DGX for aggregate CI throughput and maybe -for individual proof builds, especially with multiple runners. It is probably -overkill unless Verity needs many simultaneous full proof jobs or the same host -will serve multiple repositories. - -### What I Would Buy/Test First - -For Verity specifically: - -1. Add one modern x64 Zen 5 CPU runner with NVMe and at least 64-128 GiB RAM. - A Ryzen 9 9950X-class dedicated box is the most pragmatic first target. -2. Keep one runner service initially and test `LEAN_NUM_THREADS=8`, `12`, and - `16`. -3. Compare `lake build`, `lake build PrintAxioms`, `PrintAxioms report`, - compiler-core binary builds, and Foundry/`solc`. -4. Only add a second runner service if the host has enough cores and the first - run does not saturate memory bandwidth or disk. - -## Architecture Recommendation - -Keep the current routing: - -```yaml -fastlane: [self-hosted, linux, ARM64, dgx-spark, verity, fastlane] -build: [self-hosted, linux, x64, verity, build] -gpu: [self-hosted, linux, ARM64, dgx-spark, gpu] -``` - -Then add one new modern x64 `verity,build` host before considering a larger -topology change. - -Recommended future topology: - -```text -DGX Spark - use: primary fastlane, explicit GPU jobs, scheduled/manual ARM64 Lean validation - avoid: default full build, Foundry, x64 external-tool assumptions - -Current i7-6700 host - use: default x64 build until replaced; later fallback/secondary validation - -Current i7-7700 host - use: standby fastlane fallback and host-pinned debugging - -New Zen 5 x64 host - use: primary full x64 proof/build/compiler/Foundry CI - labels: self-hosted, linux, x64, verity, build, cpu-16-or-better, mem-64g-or-better -``` - -Do not move all CI to the DGX. Promote it for the jobs where it is already -proven and valuable: fastlane, GPU workflows, and explicit ARM64 Lean -validation. For required production proof/build CI, the next best improvement -is a modern x64 CPU runner. - -## Sources - -- NVIDIA DGX Spark User Guide, hardware and performance specifications: - -- Intel Core i7-6700 product specifications: - -- Intel Core i7-7700 product specifications: - -- AMD Ryzen 9 9950X product specifications: - -- AMD Ryzen Threadripper 9970X product specifications: - -- AMD EPYC 9575F product specifications: - diff --git a/docs/DGX_SPARK_BOTTLENECK_REPORT.md b/docs/DGX_SPARK_BOTTLENECK_REPORT.md deleted file mode 100644 index d5139e8eab..0000000000 --- a/docs/DGX_SPARK_BOTTLENECK_REPORT.md +++ /dev/null @@ -1,67 +0,0 @@ -# DGX Spark CI Bottleneck Report - -Date: 2026-05-06 -Host: `spark-de79` / `dgx-spark-gpu-1` -Repo revision measured: `3157166c` (`main`) -Tooling: Lean 4.22.0, Lake 5.0.0, `LEAN_NUM_THREADS=4` except thread-scaling tests - -## Method - -I ran Verity CI-sized tasks directly on the DGX in an isolated checkout at `/tmp/verity-dgx-profile`, using the existing runner Lean package cache and a fresh `.lake/build` when measuring cold-ish build work. Each command was wrapped by `scripts/profile_ci_resources.py`, which records wall time, GNU `time -v`, `/proc/stat` CPU/iowait samples, `/proc/meminfo`, `/proc/diskstats`, and `nvidia-smi`. - -Raw result JSON and logs were left on the DGX under `/tmp/verity-dgx-results` and copied locally to `/tmp/verity-dgx-results` during analysis. - -## Stage Timings - -| Stage | Command | Wall | Effective CPU | Peak RSS | Disk writes | GPU util | -| --- | --- | ---: | ---: | ---: | ---: | ---: | -| Python/check gate | `make check` | 33.6s | 99% | 64 MiB | 7.8 MB | 0% | -| Root Lean build, cold-ish | `lake build` | 112.4s | 188% | 1.6 GiB | 232 MB | 0% | -| Root Lean build, warm | `lake build` | 0.5s | 90% | 526 MiB | 0 MB | 0% | -| Full proof/axiom target | `lake build PrintAxioms` | 238.5s | 399% | 3.0 GiB | 733 MB | 0% | -| Axiom report execution | `lake env lean PrintAxioms.lean` | 80.2s | 104% | 1.7 GiB | 5.8 MB | 0% | -| Compiler/interpreter exes | `lake build verity-compiler verity-compiler-patched difftest-interpreter gas-report` | 80.5s | 271% | 1.8 GiB | 1001 MB | 0% | - -## Thread Scaling - -| `LEAN_NUM_THREADS` | `lake build` wall | Effective CPU | Peak RSS | -| ---: | ---: | ---: | ---: | -| 1 | 161.3s | 127% | 1.6 GiB | -| 2 | 123.5s | 169% | 1.7 GiB | -| 4 | 112.4s | 188% | 1.6 GiB | -| 8 | 110.9s | 191% | 1.6 GiB | - -More cores help up to about 4 Lean threads, then flatten. The 20-core DGX is not close to saturated for the root build; average host CPU was about 10% of 20 cores, i.e. roughly two cores. This is dependency graph and single-module elaboration limited, not hardware core-count limited. - -## Module Hotspots - -Selected module `:olean` rebuild times after priming dependencies with `lake build PrintAxioms`: - -| Module | Rebuild time | -| --- | ---: | -| `PrintAxioms` | 81.0s | -| `Compiler.Proofs.IRGeneration.SourceSemantics` | 31.3s | -| `Compiler.Proofs.IRGeneration.FunctionBody` | 19.3s | -| `Compiler.MainTest` | 17.2s | -| `Compiler.Proofs.EndToEnd` | 13.1s | -| `Compiler.Proofs.IRGeneration.Function` | 8.4s | -| `Compiler.TypedIRCompilerCorrectness` | 8.1s | -| `Compiler.Proofs.YulGeneration.Backends.EvmYulLeanBridgeLemmas` | 7.5s | -| `Compiler.Proofs.YulGeneration.StatementEquivalence` | 4.8s | - -## Bottlenecks - -1. `lake build PrintAxioms` is the largest CI-sized stage measured at 238s. It uses about four effective cores and only 3 GiB RSS, so it is compute/elaboration bound, not RAM, disk, or GPU bound. -2. `lake env lean PrintAxioms.lean` costs another 80s and is effectively single-core. The biggest actionable single target is the generated `PrintAxioms` file/report path. -3. Root `lake build` has poor scaling beyond 2-4 Lean threads. Upgrading to more CPU cores will not materially improve it unless the Lean dependency graph or hotspot modules are split. -4. Compiler executable builds write about 1 GB and use ~2.7 effective cores, but iowait stayed near zero. Disk bandwidth is not the limiting factor on the DGX NVMe. -5. `make check` is single-core Python and already short. Optimizing it is lower leverage unless it starts growing. -6. GPU utilization was 0% for all Verity CI workloads tested. GPU upgrades will not speed up these Lean/Python/compiler tasks. - -## Recommendations - -1. Optimize `PrintAxioms` first: reduce generated theorem set size where possible, split the generated file by namespace/package, or cache/report only changed theorem groups. This is the biggest direct win. -2. Split large Lean hotspots, especially `IRGeneration.SourceSemantics`, `IRGeneration.FunctionBody`, `Compiler.MainTest`, and `EndToEnd`, so Lake has more independent ready work and less single-module elaboration. -3. Keep `LEAN_NUM_THREADS` around 4 for DGX Verity CI. Higher values add little for root builds and can increase contention for other DGX work. -4. Do not buy more RAM or faster storage for this workload. Peak observed memory was under 9 GiB host-used during these runs on a 119 GiB machine, and iowait was consistently near zero. -5. Keep the DGX useful as a fast ARM64 Lean lane, but treat it as CPU-bound Lean infrastructure, not GPU acceleration infrastructure. diff --git a/docs/IDENTITY_CHECKER.md b/docs/IDENTITY_CHECKER.md index 3bff498eb3..2fc9263c5d 100644 --- a/docs/IDENTITY_CHECKER.md +++ b/docs/IDENTITY_CHECKER.md @@ -6,8 +6,8 @@ This document defines the target workflow for checking AST-level identity betwee ## Status -`scripts/generate_yul_identity_diff_report.py` is now available for deterministic, machine-readable identity diff reports suitable for CI artifacts. -`scripts/check_parity_pack_metrics.py` is now available to gate report-derived parity metrics (`onlyInVerity`, `onlyInSolidity`, `hashMismatch`) in CI. +Not implemented. A prototype report generator and metrics gate were never wired +into CI and have been removed; this page records the intended workflow. ## Goals @@ -16,22 +16,6 @@ This document defines the target workflow for checking AST-level identity betwee 3. Produce machine-readable reports for CI and rule authoring. 4. Distinguish `non-identity` from `unsupported`. -## CLI Shape - -```bash -python3 scripts/generate_yul_identity_diff_report.py \ - --solc-dir \ - --verity-dir \ - --output \ - [--fail-on-mismatch] [--max-mismatches N] - -python3 scripts/check_parity_pack_metrics.py \ - --report \ - --max-only-in-verity 0 \ - --max-only-in-solidity 0 \ - --max-hash-mismatch 940 -``` - ## Report Schema 1. `status`: `identical | non_identical`. diff --git a/docs/INTERPRETER_FEATURE_MATRIX.md b/docs/INTERPRETER_FEATURE_MATRIX.md index 0815665fb5..08fd28a540 100644 --- a/docs/INTERPRETER_FEATURE_MATRIX.md +++ b/docs/INTERPRETER_FEATURE_MATRIX.md @@ -221,7 +221,7 @@ Proof-boundary features split across two buckets. `returndataSize` reads a first 3. **Linear memory**: The IRInterpreter has single-word memory support. `mload`, `mstore`, and `returndataOptionalBoolAt` therefore remain only partially modeled in the proof interpreters today. `calldatacopy` is the exception: its destination region is modeled word-wise against the frame's calldata on both the source and IR sides, so it is fully proof-covered. `returndataCopy` now has the same region model against the frame's EIP-211 buffer on both the source and IR sides — complete words plus the padded final partial word (`returndatacopyMemoryPadded`) — with the zero-extent copy still the only one the admitted generic fragment exercises. Full linear memory coverage requires EVMYulLean semantic integration. -4. **Low-level calls**: `call`/`staticcall`/`delegatecall` and `externalCallBind`/`ecm` are compiler-only features validated by Foundry testing, not modeled in proof interpreters. The standard `Calls.callWithValue`, `Calls.callWithValueBytes`, `Calls.bubblingValueCall`, and `Calls.bubblingValueCallNoOutput` ECMs are part of this compiler-only surface for generic `call{value:v}` adapter/router flows and are surfaced as assumed ECMs in trust reports. ERC-20 optional-return helpers, including the OpenZeppelin-style and Solmate SafeTransferLib variants, also live in this ECM bucket. The EVM precompile ECMs (`Precompiles.ecrecover` 0x01, `Precompiles.sha256Memory`/`sha256` 0x02, and the BN254 curve precompiles `Precompiles.bn256Add` 0x06, `Precompiles.bn256ScalarMul` 0x07, `Precompiles.bn256Pairing` 0x08) also fall in this compiler-only ECM bucket; each precompile carries its own `evm_*_precompile` trust assumption documented in [`docs/EXTERNAL_CALL_MODULES.md`](EXTERNAL_CALL_MODULES.md) and [`AXIOMS.md`](../AXIOMS.md). `delegatecall` additionally remains a dedicated proxy / upgradeability trust boundary; use `--trust-report` / `--deny-proxy-upgradeability` when those semantics must stay outside the selected verification envelope (issue [#1420](https://github.com/lfglabs-dev/verity/issues/1420)). +4. **Low-level calls**: `call`/`staticcall`/`delegatecall` and `externalCallBind`/`ecm` are compiler-only features validated by Foundry testing, not modeled in proof interpreters. The standard `Calls.callWithValue`, `Calls.callWithValueBytes`, `Calls.bubblingValueCall`, and `Calls.bubblingValueCallNoOutput` ECMs are part of this compiler-only surface for generic `call{value:v}` adapter/router flows and are surfaced as assumed ECMs in trust reports. ERC-20 optional-return helpers, including the OpenZeppelin-style and Solmate SafeTransferLib variants, also live in this ECM bucket. The EVM precompile ECMs (`Precompiles.ecrecover` 0x01, `Precompiles.sha256Memory`/`sha256` 0x02, and the BN254 curve precompiles `Precompiles.bn256Add` 0x06, `Precompiles.bn256ScalarMul` 0x07, `Precompiles.bn256Pairing` 0x08) also fall in this compiler-only ECM bucket; each precompile carries its own `evm_*_precompile` trust assumption documented in [`docs/EXTERNAL_CALL_MODULES.md`](EXTERNAL_CALL_MODULES.md) and [`AXIOMS.md`](AXIOMS.md). `delegatecall` additionally remains a dedicated proxy / upgradeability trust boundary; use `--trust-report` / `--deny-proxy-upgradeability` when those semantics must stay outside the selected verification envelope (issue [#1420](https://github.com/lfglabs-dev/verity/issues/1420)). 5. **Internal helper compositional proofs**: `Stmt.internalCall` / `Expr.internalCall` / `Stmt.internalCallAssign` execute in the fuel-based interpreter path. Helper-level summary reuse is now surfaced as a first-class proof interface: `SupportedSpecHelperProofs.helperCallSummarySound` delivers a helper's proved summary (from the shared `hProofs` catalog) at any call site of a selector-dispatched function, and the three call-shape corollaries `evalInternalCallObeysSummary` / `execInternalCallObeysSummary` / `execInternalCallAssignObeysSummary` land that summary's postcondition directly on the interpreter result for the expression, void-statement, and assigning-statement shapes respectively. The `verity_contract` user surface for these remains ordinary direct function-name calls; `internalCall` / `internalCallAssign` are the lower-level compilation-model constructors. Broader Layer 2 completeness work continues under [#1630](https://github.com/lfglabs-dev/verity/issues/1630), with the interface/boundary refactor in [#1633](https://github.com/lfglabs-dev/verity/pull/1633). diff --git a/docs/IR_STORAGE_UINT256_REFACTOR.md b/docs/IR_STORAGE_UINT256_REFACTOR.md deleted file mode 100644 index b6dea8e691..0000000000 --- a/docs/IR_STORAGE_UINT256_REFACTOR.md +++ /dev/null @@ -1,211 +0,0 @@ -# IR Storage Type Refactor: `Nat → Nat` ⇒ `Nat → UInt256` - -Tracking the structural refactor that originally targeted the retrieve-hit and -store-hit compatibility sub-bridges of the SimpleStorage native dispatcher. -The public native SimpleStorage path has since moved to the direct native-vs-IR -`simpleStorageNativeCallDispatcherMatchBridge_of_per_case` splitter, with -`simpleStorageNativeRetrieveHitMatchBridge_proved` and -`simpleStorageNativeStoreHitMatchBridge_proved` discharging those cases. - -## Why this exists - -The first executable instantiation of the native dispatcher bridge discharged -the selector-miss compatibility bridge but left retrieve-hit and store-hit -fuel-wrapper bridges as explicit hypotheses on the public theorem -`simpleStorage_endToEnd_native_evmYulLean`. That historical fuel-wrapper route -has been removed for SimpleStorage: the public native theorem consumes direct -match proofs for selector miss, retrieve hit, and store hit. - -Those two cases cannot be discharged inside the current public theorem -signature because of a type-level mismatch: - -- The removed Verity proof oracle path used to read from - `IRState.storage : Nat → Nat` (see - [`Compiler/Proofs/IRGeneration/IRInterpreter.lean`](../Compiler/Proofs/IRGeneration/IRInterpreter.lean)). - That carrier was unbounded. -- The native path executes against EVMYulLean's `SharedState`, whose - storage carrier is `UInt256`. The native projection - (`projectStorageFromState` → `extractStorage` → `.toNat` in - [`EvmYulLeanNativeHarness.lean`](../Compiler/Proofs/YulGeneration/Backends/EvmYulLeanNativeHarness.lean)) - therefore truncates every observed value by `% UInt256.size`. -- The retrieve-hit return value is computed by `projectHaltReturn` from 32 - bytes the native path wrote with `mstore` of a `UInt256`. The raw - unbounded value has been destroyed at the `mstore` step and cannot be - recovered from the returned byte buffer alone. - -Adding a pre-state slot-bound hypothesis to the public theorem is rejected -by the transition design (the public hypothesis surface is intentionally -fixed). Routing an "expected return" through `projectResult` would make -the bridge tautological. The only resolution that closes both the -return-value and final-storage gaps without weakening the public theorem -is to bound `IRState.storage` (and the associated read/write paths) by -construction. - -## Goal - -Change the IR-side storage carrier from `Nat → Nat` to a `UInt256`-typed -carrier, so that: - -- `IRState.storage slot` and the EVMYulLean `SharedState` storage slot - agree on every observable slot up to a coercion that is provably - injective on the IR side. -- The retrieve-hit `mstore` chain on the native side and the layer-3 - oracle's storage read produce the same 32 bytes / `UInt256` payload by - reducing through a single bounded representation. -- The selector-miss bridge and any non-storage bridges remain unaffected. - -## Non-goals - -- Changing the EVMYulLean fork's storage representation. The refactor is - on the IR side only. -- Changing `verity_contract` user-facing surface syntax. Any widening of - what users can write should land in separate PRs. -- Touching transient storage / memory unless the same gap is observable - there (memory is internally `mstore`-driven and already `UInt256`-flat - in the byte buffer; transient storage is currently untouched by the - failing bridges). - -## Phased plan - -The refactor is invasive enough that it must land in well-scoped phases. -Each phase ships independently, with `lake build` and `make check` green -at every step. - -### Phase 0 — type alias and adapter surface - -Introduce a typed alias `IRStorageWord` (initially `:= Nat`) with -canonical injection/projection helpers and migrate existing -`IRState.storage : Nat → Nat` consumers to call those helpers without -changing semantics. This is a no-op rename that makes Phase 1 a single -internal-type swap. - -Deliverables: -- `Compiler/Proofs/IRGeneration/IRStorageWord.lean` — defines - `IRStorageWord`, `IRStorageWord.ofNat`, `IRStorageWord.toNat`, - `IRStorageWord.toUInt256`, plus the obvious round-trip lemmas. -- Migrate every direct `IRState.storage` field access through the new - helpers; keep the underlying carrier `Nat` so no proof has to change. - -Exit criteria: `lake build` clean; `simpleStorage_endToEnd_native_evmYulLean` -still has both `hRetrieveHit` and `hStoreHit` premises. - -Status: -- Helper module `IRStorageWord.lean` landed (commit `5de766e2`). -- `IRState.storage` field retyped to `Nat → IRStorageWord` so the - signature is already in the Phase-1-ready shape; with the current - `abbrev IRStorageWord := Nat` this is rfl-identical to `Nat → Nat`, - so no callsite needed updating. -- The "route every read/write through `IRStorageWord.ofNat` / - `IRStorageWord.toNat`" deliverable is intentionally folded into - Phase 1: under `abbrev` the routing is a no-op rename, and routing - only becomes semantically meaningful once the abbrev is removed. - -### Phase 1 — flip the carrier to `UInt256` - -Change `IRStorageWord` from `Nat` to `UInt256` (or a structurally -equivalent bounded record). Audit every site that produced an unbounded -value into storage and add the (already-true on the supported fragment) -masking step. Update the IR interpreter's `sload`/`sstore` semantics to -go through the typed helpers. - -Deliverables: -- Updated `IRStorageWord` with canonical `UInt256` representation. -- `IRInterpreter` updates so that every value entering / leaving the - storage carrier is `UInt256`-bounded. -- Audit log of every former `Nat → Nat` callsite, listed in this doc. - -Exit criteria: `lake build` clean; every existing per-contract spec -proof in `Contracts//Proofs/` still passes (no spec regressions). - -### Phase 2 — discharge retrieve hit - -With storage values bounded by construction on the IR side, the native -projection's `% UInt256.size` truncation becomes the identity on the -relevant slot. The retrieve-hit return-value chain reduces because the -`mstore`'d native bytes and the IR-oracle return word agree. - -Deliverables: -- Historical compatibility proof: - `simpleStorageNativeRetrieveHitMatchBridge_proved`. -- Public native direct proof: - `simpleStorageNativeRetrieveHitMatchBridge_proved`. -- `simpleStorage_endToEnd_native_evmYulLean` consumes the direct per-case - splitter without a retrieve-hit premise. - -Status: complete for the public native theorem. Any remaining compatibility -proof references are historical notes, not public proof authority. - -### Phase 3 — discharge store hit - -Same argument as Phase 2 for the store-hit case: the written value -flows through bounded storage and the re-read of any other materialized -observable slot agrees. The calldata round-trip was already 32-byte -bounded, so the writeback case becomes mechanical. - -Deliverables: -- Historical compatibility proof: - `simpleStorageNativeStoreHitMatchBridge_proved`. -- Public native direct proof: - `simpleStorageNativeStoreHitMatchBridge_proved`. -- `simpleStorage_endToEnd_native_evmYulLean` consumes the direct per-case - splitter without a store-hit premise. - -Status: complete for the public native theorem. Any remaining compatibility -proof references are historical notes, not public proof authority. - -### Phase 4 — generalize and retire the per-case bridge surface - -Replace the per-contract SimpleStorage direct-match family with a generic, -dispatcher-shape-driven bridge so future contracts inherit discharge -automatically. The older non-`Match` SimpleStorage bridge family is -compatibility-only and should disappear with the generic -native-proof modularization cleanup. - -Deliverables: -- Generic `nativeCallDispatcherBridge_of_typed_storage` lemma over the - supported lowered-dispatcher fragment. -- `simpleStorage_endToEnd_native_evmYulLean` re-derived from the generic - surface; no SimpleStorage-specific bridge file remains beyond the test - harness. - -## Acceptance signals - -- `simpleStorage_endToEnd_native_evmYulLean` has no `hRetrieveHit` or - `hStoreHit` premise and consumes - `simpleStorageNativeCallDispatcherMatchBridge_of_per_case`. -- `PrintAxioms` includes the direct match proofs for the public native theorem; - compatibility bridge proofs are not part of the public native theorem. -- `Contracts/SimpleStorage/Proofs/` spec theorems are unchanged. -- A second contract (e.g. Counter) lifts to the native theorem under the - generic Phase-4 surface without contract-specific bridge code. - -## Risks and rollback - -- **Spec drift.** Phase 1's flip changes the IR storage carrier; any - spec proof that implicitly relied on `Nat`-shaped storage must be - updated. Mitigation: keep Phase 0's helper surface stable so spec - proofs only see the helper API. -- **Performance.** `UInt256` arithmetic in the IR interpreter is heavier - than `Nat` arithmetic for `decide`-style proof automation. Mitigation: - expose `simp` lemmas that re-export bounded results as `Nat` only at - the spec boundary. -- **Rollback.** Phase 0 is a pure rename; Phase 1 is the only - destructive step. If Phase 1 hits an unexpected proof regression that - cannot be repaired in the same PR, revert the Phase 1 commit and keep - the helper surface in place for a later attempt. - -## Out-of-scope follow-ups - -- Memory carrier typing. Memory is currently `Nat → Nat` for similar - reasons and benefits from the same treatment, but the failing bridges - do not depend on it. -- Transient storage carrier typing. Same caveat. -- EVMYulLean fork changes. The refactor stays on the Verity side. - -## See also - -- [`Compiler/Proofs/EndToEnd.lean`](../Compiler/Proofs/EndToEnd.lean) - — `simpleStorage_endToEnd_native_evmYulLean` and the - `simpleStorageNative*Bridge` family. -- [`Compiler/Proofs/IRGeneration/IRInterpreter.lean`](../Compiler/Proofs/IRGeneration/IRInterpreter.lean) - — the current `IRState.storage : Nat → Nat` definition. diff --git a/docs/LANGUAGE_DESIGN_AXES.md b/docs/LANGUAGE_DESIGN_AXES.md deleted file mode 100644 index fb5d01c27e..0000000000 --- a/docs/LANGUAGE_DESIGN_AXES.md +++ /dev/null @@ -1,150 +0,0 @@ -# Language Design Axes — Implementation Plan - -> Tracking issues: #1726 (umbrella), #1727 (Axis 1), #1728 (Axis 2), #1729 (Axis 3), #1730 (Axis 4) -> Connects to: #1724 (Solidity parity), #1723 (proven fragment extension) - -## Overview - -This document describes the plan to take Verity's `verity_contract` EDSL beyond Solidity feature parity by fixing Solidity's systemic design flaws at the language level. Four axes of improvement have been identified, each with concrete features, implementation strategies, and integration points with the existing codebase. - -**Design philosophy**: Easy to translate from Solidity, hard to introduce Solidity's bugs. - -## Suggested Implementation Order - -The order is driven by **impact x effort x unblocking power**: - -### Phase 1: Effect System & Auto-Theorem Generation (Axis 3, #1729) - -**Why first**: Lowest friction, highest proof payoff. - -- `@[view]` verification already exists in `Validation.lean` (`stmtWritesState`) — the infrastructure is 80% there -- Frame condition theorems are the #1 bottleneck in compositional verification — every annotated function instantly contributes to #1723 -- No `verity_contract` grammar changes needed — only extends existing function-level attributes -- Builds the **auto-theorem generation machinery** that Axis 2 reuses - -| Step | Feature | Effort | Detail | -|---|---|---|---| -| 1a | Strengthen `@[view]` with auto-generated `_is_view` theorem | 1 week | Keep existing validation; add theorem emission in `Macro/Elaborate.lean` | -| 1b | Add `@[modifies slot1, slot2]` with `_frame` theorem | 2–3 weeks | New validation pass (enumerate write targets vs declared set); generate universally-quantified frame theorem | -| 1c | Add `@[no_external_calls]` with `_no_calls` theorem | 1 week | Reject external call statements; generate no-calls theorem | -| 1d | Annotation composition | 1 week | Multiple annotations on one function; conjunction theorem | - -**Files touched**: `Macro/Elaborate.lean`, `Macro/Translate.lean`, `CompilationModel/Validation.lean` -**Estimated total**: 3–4 weeks - -### Phase 2: CEI Enforcement + Access Control (Axis 2 partial, #1728) - -**Why second**: Reuses Phase 1's theorem generation infrastructure. - -| Step | Feature | Effort | Detail | -|---|---|---|---| -| 2a | CEI static analysis | 1–2 weeks | Walk function body AST, track external call position, error on writes after calls | -| 2b | CEI opt-out ladder | 1 week | `@[cei_safe by proof]` (machine-checked), `nonReentrant` recognition (low trust), `@[allow_post_interaction_writes]` (trust surface) | -| 2c | `roles` section + `@[requires Role]` | 2–3 weeks | New grammar section; macro-generated require + auto-generated access control theorem | - -**Escalation ladder** (consistent across all safety features): -``` -SAFE BY DEFAULT (compiler enforces) - → LEAN PROOF (machine-checked, zero trust) - → KNOWN-SAFE GUARD (nonReentrant, require) - → EXPLICIT ANNOTATION (trust surface in --trust-report) -``` - -**Files touched**: `Macro/Syntax.lean` (roles section), `Macro/Translate.lean`, new `Macro/CEICheck.lean` -**Estimated total**: 4 weeks - -> **Scope note (2026-06)**: the 2b ladder above governs *single-function* -> Checks-Effects-Interactions ordering only. It does **not** prevent -> cross-function reentrancy (the Midnight `take`/`liquidate` class), so -> `cei_safe`/`allow_post_interaction_writes` are intentionally **not** accepted -> by the cross-function reentrancy gate (`validateReentrancyDisposition`); that -> gate's sound accept-set is `nonreentrant()` or `reentrancy_trusted`. -> See the "Cross-Function Reentrancy Gate" sections of `AUDIT.md` / -> `TRUST_ASSUMPTIONS.md`. - -### Phase 3: Semantic Newtypes (Axis 1 partial, #1727) - -**Why third**: Standalone grammar change, doesn't depend on Phase 1–2. - -| Step | Feature | Effort | Detail | -|---|---|---|---| -| 3a | `types` section in grammar | 1 week | Add to `Macro/Syntax.lean`; parse `TypeName : BaseType` entries — **done** (dc4b40d2) | -| 3b | `ParamType.newtypeOf` + `ValueType.newtype` | 1 week | Extend `ParamType` inductive with `newtypeOf` constructor; add `ValueType.newtype`; wire through all exhaustive pattern matches across 11 files — **done** | -| 3c | Type checking + Yul erasure | 1 week | Newtypes erased to base type at every Yul boundary (zero overhead); `#check_contract NewtypeSmoke` passes — **done** | - -**Key insight from research**: `ParamType` is a closed inductive with 13 variants (including `adt` and `newtypeOf`) and `valueTypeFromSyntax` resolves user-defined type names via `NewtypeDecl` lookup. The `newtypeOf` constructor is erased to `baseType` at every compilation boundary. - -**Files touched**: `CompilationModel/Types.lean`, `CompilationModel/AbiTypeLayout.lean`, `CompilationModel/AbiHelpers.lean`, `CompilationModel/AbiEncoding.lean`, `CompilationModel/ParamLoading.lean`, `CompilationModel/EventAbiHelpers.lean`, `CompilationModel/ScopeValidation.lean`, `CompilationModel/ValidationCalls.lean`, `Compiler/ABI.lean`, `Compiler/TypedIRCompiler.lean`, `Verity/Macro/Translate.lean` -**Estimated total**: 2–3 weeks - -### Phase 4: Namespaced Storage (Axis 4, #1730) - -**Why fourth**: Standalone, doesn't depend on other phases. - -| Step | Feature | Effort | Detail | -|---|---|---|---| -| 4a | Namespace computation | 1 week | Compute `keccak256("{ContractName}.storage.v0")` at elaboration time using kernel Keccak | -| 4b | Slot offsetting | 1 week | All `slot N` declarations become `slot (namespace_base + N)` | -| 4c | Override syntax | 0.5 weeks | `storage_namespace legacy` opts out; `storage_namespace "custom"` and `storage_namespace erc7201 "custom"` select explicit roots | -| 4d | ABI + tooling integration | 0.5 weeks | Emit namespace in layout reports via `--layout-report`; opt in to automatic roots with `set_option verity.storageNamespace.default true` | - -**Files touched**: `Macro/Elaborate.lean`, `Macro/Translate.lean`, `CompilationModel/AbiHelpers.lean` -**Estimated total**: 2–3 weeks - -### Phase 5: ADTs + Pattern Matching + Result Types (Axis 1 remainder, #1727) - -**Why last**: Largest change, touches the most files, benefits from all prior infrastructure. - -| Step | Feature | Effort | Detail | -|---|---|---|---| -| 5a | `inductive` section in grammar | 1 week | Parse variant definitions with typed fields | -| 5b | New CompilationModel constructs | 2 weeks | `ParamType.adt`, `Expr.adtConstruct/adtTag/adtField`, `Stmt.matchAdt` | -| 5c | Storage encoding (tagged unions) | 1 week | Tag byte + max-width fields in consecutive slots | -| 5d | Yul lowering | 1 week | `matchAdt` → `YulStmt.switch`; `adtConstruct` → sequential `sstore` | -| 5e | ABI encoding | 1 week | `(uint8 tag, fields...)` with max-width encoding | -| 5f | `Call.Result` + `!` sugar | 2 weeks | Built-in ADT for external call results; `externalCall!` desugars to match+revert | - -**Key design decisions**: -- Storage layout uses max-width of all variants (like Rust enum layout) -- Exhaustiveness is free — Lean's kernel checks it during elaboration -- `YulStmt.switch` already exists in the Yul AST (used by the dispatcher) — no Yul-level changes needed - -**Files touched**: `Macro/Syntax.lean`, `CompilationModel/Types.lean`, `Macro/Translate.lean`, `CompilationModel/Compile.lean`, `CompilationModel/Validation.lean`, `CompilationModel/AbiHelpers.lean` -**Estimated total**: 6–8 weeks - -### Phase 6: Unsafe Blocks (Axis 2 remainder, #1728) - -| Step | Feature | Effort | Detail | -|---|---|---|---| -| 6a | `unsafe "reason" do` syntax | 1 week | Add to grammar; track safe/unsafe context | -| 6b | Restricted operation gating | 1 week | Error if `delegatecall`/raw `sstore`/raw `mstore`/`rawLog` appears outside `unsafe` | -| 6c | Trust report + deny flag | 0.5 weeks | Each `unsafe` block → `--trust-report` entry; `--deny-unsafe` flag | - -**Files touched**: `Macro/Syntax.lean`, `Macro/Translate.lean`, trust report infrastructure -**Estimated total**: 2 weeks - ---- - -## Total Estimated Timeline - -| Phase | Axis | Weeks | Parallelizable with | -|---|---|---|---| -| 1 | Effect system (#1729) | 3–4 | — | -| 2 | CEI + access control (#1728) | 4 | Phase 3 | -| 3 | Newtypes (#1727) | 2–3 | Phase 2 | -| 4 | Namespaced storage (#1730) | 2–3 | Phase 5 | -| 5 | ADTs + Result types (#1727) | 6–8 | Phase 4 | -| 6 | Unsafe blocks (#1728) | 2 | Phase 5 | - -**Critical path**: Phase 1 → Phase 2 → Phase 5 (13–16 weeks) -**With parallelization**: ~12–14 weeks total - -## Integration with #1724 Parity Waves - -| #1724 Wave | Language Design Enrichment | -|---|---| -| Wave 1: Type System (widths, enum) | + Newtypes (Phase 3), ADTs (Phase 5) | -| Wave 2: Control Flow & Safety | + CEI (Phase 2), `@[requires]` (Phase 2), effect annotations (Phase 1) | -| Wave 3: Storage | + EIP-7201 namespaces (Phase 4) | -| Wave 4: ABI & Memory | + `Call.Result` + `!` sugar (Phase 5) | -| Wave 5: Composition | + `unsafe` blocks (Phase 6) | diff --git a/docs/PANIC_SURFACE_1999_SPEC.md b/docs/PANIC_SURFACE_1999_SPEC.md deleted file mode 100644 index 54638b7214..0000000000 --- a/docs/PANIC_SURFACE_1999_SPEC.md +++ /dev/null @@ -1,107 +0,0 @@ -# Panic Surface #1999 Spec - -## Implemented Contract - -Compiler-generated unsigned checked arithmetic uses a closed panic-code domain through -the compilation model and typed IR: - -- `Verity.Core.PanicCode` has exactly two constructors: - `arithmeticOverflow` maps to `0x11`, and `divisionByZero` maps to `0x12`. -- `Stmt.panic PanicCode` and `TStmt.panic PanicCode` carry this type until Yul - lowering. The conversion to a numeric ABI word is explicit at that boundary. -- `addPanic`, `subPanic`, and `mulPanic` lower to - `Stmt.panic .arithmeticOverflow`; `divPanic` lowers to - `Stmt.panic .divisionByZero`. -- The checked-arithmetic model is an exact pair of adjacent statements: a - guarded typed panic followed by the arithmetic result binding. Together, - those statements form the guard/panic/arithmetic sequence. -- Typed panics lower directly to the canonical `Panic(uint256)` body. The body - stores selector `0x4e487b71` at offset 0, stores code `0x11` or `0x12` at - offset 4, and executes `revert(0, 36)`. -- Usage analysis and the post-codegen peephole recognize only the exact typed - shape with matching guard, operands, operation, and panic constructor. - Standalone, reversed, mismatched, wrong-code, and raw-panic lookalikes do not - enable the checked helpers. `Stmt.unsafeYul` fragments are lowered with - comment-only provenance markers and are optimizer-opaque, so handwritten - Yul cannot be rewritten as a typed helper call. -- ECM output uses separate comment boundaries and is inspected automatically, - without an author opt-in. Rewrites require numeric-literal or variable-reference - operands and the exact guard/panic/arithmetic shape. Calls remain unchanged. - Each emitted deployment/runtime section must contain exactly one canonical - definition of all four arithmetic helpers and both panic helpers, with no - conflicting function, local, parameter, or return binding anywhere in that - section. Missing or altered helpers disable the pass for that section; ECM - alone does not trigger helper insertion. Rewrites never cross region markers. - Malformed markers disable the pass; reserved ECM marker text anywhere in a - module's output makes that module opaque. Explicit unsafe-Yul regions remain - opaque, including when nested in ECM output. - -The direct Lean implementations in `Verity.Stdlib.Math` retain their existing -diagnostic strings for executable/model use. Those strings are not the compiled -contract's revert encoding: macro-generated contracts bypass that compatibility -path and emit canonical `Panic(uint256)` bytes rather than `Error(string)`. - -## Raw Compatibility Boundary - -The typed arithmetic domain intentionally coexists with the raw expression -constructors `Stmt.panicCode Expr` and `TStmt.panicCode (TExpr .uint256)`. -General `panic(code)` syntax, runtime-supplied codes, and generated enum-range -guards such as `panic(0x21)` remain raw. Raw lowering still evaluates and caches -the code expression before constructing the canonical payload. A raw literal -`0x11` therefore does not become typed arithmetic evidence and is not rewritten -by the checked-arithmetic peephole. - -This separation keeps the local two-code specification strict without claiming -that the full Solidity panic-code catalog has been modeled as `PanicCode`. - -## Proof and Regression Coverage - -- `Compiler.Proofs.IRGeneration.PanicPayloadIR` proves the abstract IR memory - updates at offsets `0` and `4`, preservation of other abstract memory entries, - and a revert result for both typed constructors and every in-range numeric - code. That interpreter does not retain the revert offset, length, or bytes. -- `Compiler.Proofs.YulGeneration.PanicPayloadBytes` reads the actual emitted - payload AST and uses EVMYulLean's byte-addressed `mstore` and `evmRevert` - operations. It proves that, for any initial memory, the returned bytes are - exactly `[0x4e, 0x48, 0x7b, 0x71]` followed by the 32-byte code word, with - total length 36. The general theorem interprets numeric codes as EVM words; - typed specializations cover `0x11` and `0x12`. This is a proof of the local - panic instruction sequence, not whole-contract execution or solc bytecode. -- Byte-level regressions check both typed payloads and distinguish empty, - truncated, and wrong-offset reverts. Separate characterization examples show - why the abstract IR result alone cannot establish this byte-level guarantee. -- Feature tests cover all four arithmetic wrappers, both typed code mappings, - direct typed lowering, cached raw lowering, raw runtime panic, raw enum code - `0x21`, and mixed typed/raw Yul where only the typed pair is rewritten. - -## Completion Criteria - -This slice is complete when all of the following hold: - -1. Checked arithmetic is typed through the model and typed IR, with the exact - two-constructor mapping above. -2. Both typed payloads have canonical backend proof and regression coverage. -3. Runtime raw panic and generated `0x21` enum behavior remain compatible. -4. The audit, trust, axiom, and arithmetic-profile documents are synchronized, - and the trust/documentation checks pass. - -## Independent Follow-ups - -These are separate proof projects and do not block this slice: - -1. Prove that the checked-arithmetic Yul peephole rewrite preserves semantics. - Its current assurance is exact fail-closed structural matching plus positive - and adversarial regression tests. This is the higher-priority follow-up. -2. Admit typed panic statements into the generic whole-contract `SupportedSpec` - proof fragment. Payload correctness alone does not establish that broader - source-to-IR theorem. -3. Add a shared source-model byte-level revert observable and connect it to the - proved backend payload. -4. Model more of Solidity's panic-code catalog as typed constructors if a later - feature requires it. - -No project-level axiom is added by this implementation. - -The signed `Int256` wrappers retain their existing type-specific guards and -diagnostic-string revert lowering. Migrating that separately introduced surface -to typed panic payloads is outside this unsigned-arithmetic slice. diff --git a/docs/ROADMAP.md b/docs/ROADMAP.md index 1b5a1090e8..73ddbb3e0c 100644 --- a/docs/ROADMAP.md +++ b/docs/ROADMAP.md @@ -34,8 +34,8 @@ surfaces (the txOrigin postmortem). Landed so far on this branch: `verity-compiler` (patch pass is a proven identity when disabled). - ✅ `SupportedFunction.noNonReentrant`: the guarded-functions-outside- SupportedSpec boundary is machine-checked. -- ✅ `Contracts/Smoke.lean` split by feature surface; doc-sync consolidation - inventory at `scripts/consolidation-inventory.md`. +- ✅ `Contracts/Smoke.lean` split by feature surface; doc-sync checks + consolidated into `scripts/docsync.py`. Next: derive the executable shallow program from the deep model (`Stmt.denote`, macro retarget per-contract behind a flag), collapse the @@ -122,8 +122,8 @@ Current P1 foundation coverage (Issue #582): - Report-capable compiler path via `Compiler.emitYulWithOptionsReport` to surface patch manifest/iteration metadata to CI and tooling - `verity-compiler` patch coverage emission (`--patch-report`) now writes per-contract/rule TSV output and CI uploads it as an artifact for Issue #583 tuning - Static gas delta gate for patch impact (`scripts/check_patch_gas_delta.py`) now compares baseline vs patch-enabled reports in CI with median/p90 non-regression and measurable-improvement requirements -- CI now runs `check_yul.py` + `check_gas.py coverage` on `artifacts/yul-patched` as part of Issue #582 fail-closed hardening for patch-enabled output, including filename-set parity checks against baseline Yul output -- CI now runs a dedicated Foundry patched-Yul smoke gate (`DIFFTEST_YUL_DIR=artifacts/yul-patched`) so differential/property harnesses execute against patch-enabled output +- CI now runs `check_yul.py` + `check_gas.py coverage` on `compiler/yul-patched` as part of Issue #582 fail-closed hardening for patch-enabled output, including filename-set parity checks against baseline Yul output +- CI now runs a dedicated Foundry patched-Yul smoke gate (`DIFFTEST_YUL_DIR=compiler/yul-patched`) so differential/property harnesses execute against patch-enabled output Execution policy: 1. Do not start patch-pack expansion in `#583` before `#582` proof hooks are merged. @@ -384,7 +384,6 @@ A comprehensive feature matrix documents which CompilationModel constructs each - **Human-readable**: [`docs/INTERPRETER_FEATURE_MATRIX.md`](INTERPRETER_FEATURE_MATRIX.md) - **Machine-readable**: `artifacts/interpreter_feature_matrix.json` -- **Smoke tests**: `Compiler/Proofs/InterpreterFeatureTest.lean` (22 `native_decide` proofs) Key: the default path is `execStmtsFuel` (fuel-based), which supports the full construct set including loops and internal calls. The basic `execStmts` path is used only for proofs that do not need these features. @@ -416,7 +415,7 @@ Execution priorities: ### ✅ **ERC-4337 Frame Primitives Landed** **What**: Reusable primitives extracted from the ERC-4337 EntryPoint benchmark. **Status**: Landed as `Verity.EVM.Frame`, `Verity.EVM.MemoryModel`, -`Verity.EVM.Layout`, `Verity.Trace`, `Verity.Compiler.FromSolidity`, plus +`Verity.EVM.Layout`, plus `nonReentrantTransient` in `Verity.Core` and a `let _ := …` discard rule in `Contracts.Common`. Each closes a trust gap for every benchmark, not just ERC-4337. @@ -426,10 +425,6 @@ ERC-4337. - **EvmYul ↔ frame correspondence**: prove `Verity.EVM.Frame.applyCallToCaller` is the projection of EvmYul's `CALL` opcode semantics through the caller frame. Closes the abstract-model gap. ~2-4 weeks. -- **Machine `fromSolidity`**: replace the CLI scaffold in - `Verity.Compiler.FromSolidity` with an in-process Solidity → Verity IR → - `verity_contract` translator. Multi-week. Tracked as the canonical - follow-up to the scaffold landed here. - **`solc_disjoint` tactic**: today `Verity.EVM.Layout.call_buffer_disjoint_from_heap` discharges the disjointness premise from `SolcLayout` invariants. A proper tactic that closes `MemoryModel.Disjoint` goals automatically would diff --git a/docs/SOLIDITY_IMPORT.md b/docs/SOLIDITY_IMPORT.md new file mode 100644 index 0000000000..dd3621e015 --- /dev/null +++ b/docs/SOLIDITY_IMPORT.md @@ -0,0 +1,219 @@ +# Solidity import + +`solidity_import` turns selected Solidity functions, and the helpers they +reach, into an ordinary Verity `CompilationModel`. The model runs on the same +`Denote` semantics as Verity's proofs, so theorems about it are theorems about +the imported code. The importer lives in `Compiler/SolidityImport/`; the +Midnight pilot (`lfglabs-dev/morpho-midnight-verity`) is the reference user. + +## Usage + +```lean +import Compiler.SolidityImport.Import + +solidity_profile build where + evmVersion := "osaka" + viaIR := true + optimizerRuns := some 466 -- none = optimizer off + bytecodeHash := "none" + +solidity_import example from "Contracts/SolidityImportSmoke" entry "Slice.sol" using build + contract C + function f(Mkt, bytes32, address) + function lossOf(bytes32) +``` + +- `from` is the Solidity project directory, relative to the importing + package's `lakefile.lean`; its `remappings.txt` resolves imports. `entry` is + the file declaring the contract. +- `solidity_profile` defines an ordinary `Profile` value (reusable, + `#print`able). `using` takes any `Profile`, named or inline + (`using { evmVersion := "osaka", ... }`). The settings go to solc and into the + digest; they should match how the audited bytecode is built. `solc` defaults + to the only accepted release, `0.8.34+commit.80d5c536`. +- Each `function` clause selects one root by name and Solidity parameter types. + A struct may be qualified (`IMidnight.Market`). Roots are lowered + independently and share one field list. + +The command defines `example.model`, `example.report` (`toText` renders the +reviewed inventory), `example.sourceDigest`, and the kernel theorem +`example.covered`. It works inside namespaces. + +It also defines typed accessors for specifications, with Solidity types +(`UIntN n`, `Uint256`, `Address`, `BytesN 32`, `Bool`): + +- `example.f oracle world m_maturity id user : Option (UIntN 128 × UIntN 128 × UIntN 128)` + calls the imported function `f` in `world` (block timestamp included); + `none` means it reverts. +- `example.position.credit oracle world id user : UIntN 128` reads + `position[id][user].credit` with the imported layout (solc's slot, word + offset and packing); `example.position.credit_val` states that its value is + exactly the model's read. Key binders use the Solidity key names. + +`oracle` computes mapping slots and is a parameter, so a theorem over these +accessors holds for any slot derivation; it does not identify slots with Keccak. + +Install the compiler once with `python3 scripts/setup_solc_import.py` (a +downstream package passes `--output .lake/solidity-import/solc-0.8.34`). +Elaboration never downloads a compiler; it checks the binary's SHA-256 before +and after running it. + +Declare the Solidity tree as a Lake input so editing a source rebuilds the +import: + +```lean +input_dir midnightSol where + path := "vendor/midnight/src" + filter := .extension "sol" +lean_lib MorphoMidnight where + needs := #[midnightSol] +``` + +## Proving properties of an imported function + +State properties with the typed accessors above. The accessors unfold to +`runFunction` and `readMember` (`Compiler.SolidityImport.Access`), and +`evalExpr_structMember2_param` rewrites the model's own storage reads to +`readMember`. `Compiler.SolidityImport.Proofs` provides the lemmas used to +reason about the execution: word arithmetic (`sub_word`, +`mul_word128`, `div_word`, `mask_eq`), bindings (`lookup_bind_same`, +`lookup_bind_other`), and splitting a body into straight-line parts +(`split_prefix`, `list_frame`, `ends_return`). Solidity locals keep their name +in the model, so a proof can refer to `postSlashCredit` rather than to a +generated temporary. See +[morpho-midnight-verity](https://github.com/lfglabs-dev/morpho-midnight-verity) +for a complete example. + +## Supported Solidity + +This is deliberately the subset Midnight's `updatePositionView` needs, not a +claim to support arbitrary Solidity. + +| Construct | Lowering | +| --- | --- | +| Explicit scalar/tuple return | `returnValues`, preserving order | +| Local declarations and storage aliases | Bindings named after the Solidity local (suffixed `_1`, `_2`, ... on collision), or resolved read paths | +| One/two-key mappings to structs | solc slots, word offsets, and packed uint offsets | +| Scalar member of a memory/calldata struct parameter | Explicit scalar projection; no ABI decoder | +| Unsigned `+`, `-`, `*`, `/` | Word arithmetic with overflow/underflow/zero-divisor panics | +| Unsigned comparisons, equality | Scalar conditions | +| Narrowing casts | Bit masks, not overflow checks | +| Ternaries | Lazy `ite` branches | +| Resolved acyclic helper calls | Inlined bodies with separate local scopes | +| Single assignment to a named assembly return | `xor`, `mul`, `lt`, as in `UtilsLib.min` | + +Each root must return explicitly. Loops, state writes, external calls, +modifiers, recursion, virtual dispatch, named call arguments, signed +operations, and any other construct reached from a root are rejected with +`file:line:column`, the construct, the reason, and the call path from the root +(`closure: C.f -> L.unused`). Functions outside the closure are listed as +excluded and do not block the import. Only reached storage fields are decoded; +array/mapping struct members are reported as opaque and cannot be read. + +## What each check establishes + +Four questions are answered separately, per root; none implies the next. + +1. **Importable.** The command succeeds, or fails with the diagnostic above. + A partial model is never produced, so every listed root is importable. +2. **Runs on Denote** (`denoteCovered`). Every constructor in the body has an + explicit denotation arm (not the catch-all). The kernel proof for all roots + together is `x.covered`. +3. **Compiles** (`compilable`). The ordinary Verity compiler accepts the root on + its own. This is a compile attempt, not a correctness result; agreement + with Denote and solc is only sampled by the A/B/C campaigns below. +4. **Covered by compiler proofs** (`compilerProofCovered`). Always + `unavailable`: the importer builds no `SupportedSpec` witness and + `SupportedSpec` has no decision procedure. The report never implies it. + +`x.report` records 1, 2 and 4 (`status` lines of `toText`). Compilation depends +on the compiler, not the import, so the differential driver reports all four: + +```lean +#eval IO.print (Differential.statusText x.model x.report) +``` + +```text +function f + importable true + denoteCovered true + compilable true + compilerProofCovered unavailable (the importer builds no SupportedSpec witness; compiled code is only tested by A/B/C) +``` + +## Trust boundary + +solc's AST and storage layout, and the lowering in `Import.lean`, are trusted. +The model is built as ordinary values and quoted (`Quote.lean`); after +elaboration the definitions are evaluated back and must equal those values. +The digest is SHA-256 over framed JSON of the complete solc input, the selected +signatures, the solc release, and the sources of `Import.lean`, +`Coverage.lean`, `Report.lean`, `Quote.lean` and `Profile.lean`. Panic payloads, +gas, public ABI decoding and bytecode equivalence are outside the model. + +Consumers should re-elaborate in CI, check the fresh model is `rfl`-equal to +the compiled import, and keep a reviewed inventory (`report.toText`) and a +golden `repr` of the model, as the pilot's `check_import.py` does. + +## Differential validation + +Tests supplement proofs; no test count establishes equivalence. Every case +runs three paths: + +- **A:** the original Solidity, compiled by the pinned solc, run in Foundry. +- **B:** the imported model, run by `Denote.execStmtList` (the proofs' + semantics) with Verity's pure Keccak for mapping slots. +- **C:** the model compiled by Verity to Yul, then by solc, run in Foundry. + +A consumer supplies a JSON fixture (see +`Contracts/SolidityImportSmoke/differential.json`): source and signature, +input domains, argument projections, storage recipes and a fixed corpus. +Python builds calldata with `eth-abi` and storage slots with `eth-hash`, +independently of Verity. Unused packed bits, neighbouring words and unrelated +slots get noise. Each EVM case runs from a restored snapshot and must not +write storage. + +A/B/C must agree on success/revert, return words and observed storage; A and C +must also agree on revert bytes. Timeouts, resource limits, unsupported +compilation and tool failures are harness errors, never reverts. + +```sh +scripts/check_solidity_differential.sh --config Contracts/SolidityImportSmoke/differential.json \ + --output .lake/d/smoke --seed 2438 --cases 128 # inputs +scripts/check_solidity_differential.sh --programs 5 --cases 16 --seed 2438 \ + --output .lake/d/programs # generated programs +scripts/check_solidity_differential.sh --mutations --output .lake/d/mutants +scripts/check_solidity_differential.sh --replay --output .lake/d/smoke +scripts/check_solidity_differential.sh --reduce --reduce-seconds 120 --output .lake/d/failing +``` + +- **Generated programs** cover uint8/16/128/248/256 casts, checked arithmetic, + ternaries and helpers. Each also runs renamed and helper-extracted variants + that must behave identically (`metamorphic-divergence.json` on mismatch). +- **Replay and reduction.** A campaign records seeds, inputs, sources, solc + input/output, Yul, bytecodes, tool versions and hashes. Replay refuses + changed artifacts. The reducer shrinks inputs (and generated expression + trees) while preserving the mismatch category. +- **Mutants** are real importer and Denote edits built in isolated copies. + Each must produce a runtime divergence; one that fails to compile is + invalid, not detected. +- **Importer regressions:** `python3 scripts/solidity_import_mutations.py` + checks the golden model (`--update-golden` after review), hygiene, + determinism, rejections with diagnostics, multi-root independence, and that + source changes move the witness results and the digest. + +CI runs fixed seeds on every PR (10 minutes) and longer input, program and +mutant campaigns nightly and on demand (60 minutes), archiving the evidence. +A timeout fails the job. + +## Adding a construct + +A new construct needs, in the same PR: positive, rejection, boundary and +interaction cases; an A/B/C fixture or generated-program coverage; a mutant +when it carries real risk; and the Lean proofs it enables. Keep the smoke +golden model and the Midnight proofs green, and turn every reduced +counterexample into a permanent regression. + +EVMYulLean has a bytecode executor (`EvmYul.EVM.Ξ`) that could become a fourth +path; it would need account/block setup, opcode conformance checks and +independent result decoding, and must not replace Foundry. diff --git a/docs/SOLIDITY_IMPORT_ROADMAP.md b/docs/SOLIDITY_IMPORT_ROADMAP.md deleted file mode 100644 index 09ca837d86..0000000000 --- a/docs/SOLIDITY_IMPORT_ROADMAP.md +++ /dev/null @@ -1,849 +0,0 @@ -# Solidity import roadmap: from the Vault POC to the Pareto Credit Vault - -**Status**: planning document. This file commits to a slice order, the files -each slice touches, the tests and acceptance evidence each slice must ship, -and what each slice explicitly leaves out. It contains no importer code. - -**Goal**: a minimal proof-only Solidity import of `IdleCDOCreditVault`, -`IdleCDOEpochVariant` and `IdleCreditVault` from -[Idle-Labs/idle-tranches @ `54502d1`](https://github.com/Idle-Labs/idle-tranches/tree/54502d1) -together with their OpenZeppelin 4.9.6 upgradeable parents, built by -extending the existing proof-only importer under -[`Contracts/VaultFromSolidity`](../Contracts/VaultFromSolidity). - -**Inputs** - -- POC: [`Contracts/VaultFromSolidity`](../Contracts/VaultFromSolidity) - (`Importer/Importer.lean`, `Importer/Syntax.lean`, - `Importer/Semantics.lean`, `Spec.lean`, `Proofs/ExecutionProof.lean`, - `Importer/scripts/solidity_importer_test.py`). -- Feature list: `audit/VERITY-GAPS.md` in - `lfglabs-dev/pareto-credit-vault-proof-closure` - (branch `assessment/source-faithful-accounting`, read-only), rows G1–G18. -- Existing `verity_contract` support for the same contracts: - [`docs/PARETO_MODEL_SUPPORT.md`](PARETO_MODEL_SUPPORT.md), - [`docs/MODIFIERS_AND_INHERITANCE.md`](MODIFIERS_AND_INHERITANCE.md), - [`docs/EXTERNAL_CALL_MODULES.md`](EXTERNAL_CALL_MODULES.md). - -**Slice order** (fixed): inheritance → modifiers/structs → external calls and -try/catch → signed integers → solc 0.8.10 → one Pareto smoke. - ---- - -## 1. Hard rules for every slice - -These apply to every PR on this roadmap, including the last one. - -1. **No `sorry`, no `admit`, no `native_decide`, no new axioms.** The - axiom audit in `solidity_importer_test.py` (every theorem in every - importer proof file reports only `propext` and `Quot.sound`) is extended - to each new smoke and stays a hard CI gate. External-call behaviour is a - *parameter* of the imported definitions (Section 4.3), never an axiom. - `AXIOMS.md` keeps "Active axioms: 1". -2. **The POC stays green.** `lake build VaultFromSolidity` and the full - `solidity_importer_test.py` run must pass unchanged on every slice. The - seven Vault theorems are the regression baseline for `solidity_simp`. -3. **The accepted subset stays closed and intrinsically typed.** New - constructs are new constructors in `Syntax.lean` with one meaning in - `Semantics.lean`. No generated Lean source, no serialized IR, no - `unsafe`, no `partial` definitions in the semantics. Kernel checking stays - synchronous (`Elab.async := false` inside the import transaction) with - full environment rollback on failure. -4. **Fail closed with a source position.** Every rejection is a - `path:line:col` diagnostic. The AST schema stays an allowlist - (`allowedNodeFields` / `requiredNodeFields`); a new node kind is accepted - only when its meaning is defined. -5. **The digest covers the whole trusted translation.** `sourceDigest` - keeps hashing source bytes, solc output, `Importer.lean`, `Syntax.lean`, - `Semantics.lean`, and the compiler pin. Any new trusted file (for example - a version-indexed schema table) is added to the digest and to the - "content change invalidates artifacts" mutation checks. -6. **Docs move with the boundary.** Each slice updates - `TRUST_ASSUMPTIONS.md` (the "Proof-only Solidity Vault import" section), - `AUDIT.md`, the `AXIOMS.md` audit paragraph, `README.md`'s importer - section, `docs/VERIFICATION_STATUS.md`, `scripts/check_contract_structure.py` - exemptions, and regenerates `PrintAxioms.lean` and - `artifacts/verification_status.json` when theorem counts change. -7. **Git discipline.** One branch per slice from current `origin/main`, one - PR per slice (S2 may be split as noted), never rebase or force-push a - published branch, merge commits from `main` only. Proofs under 30 lines - or allowlisted per `scripts/check_proof_length.py`. - ---- - -## 2. Where we are: the POC - -`solidity_contract Alias from "Vault.sol"` runs pinned solc 0.8.33 -(`--standard-json --no-import-callback`, optimizer off, `evmVersion: cancun`), -validates the typed AST and storage layout against a closed schema, parses -each function body into the intrinsically typed inductive in `Syntax.lean`, -and registers each entry point as `Sol.Fn.meaning body slots env`. It also -elaborates a kernel-checked `Storage` structure, a `view : ContractState → -Storage`, and the entry-point relation `step`. - -Accepted today: - -| Area | Accepted | Rejected (diagnostic) | -|------|----------|-----------------------| -| Source unit | one pragma `^0.8.33`, exactly one non-abstract contract, no bases | imports, several contracts, `abstract`, `is` | -| Storage | `uint256` scalars, `mapping(address => uint256)`, offset 0, 32-byte slots | any other type, packed fields, initializers, constants, `layout at` | -| Declarations | zero-argument custom errors, public getters | events, modifiers, structs, enums, `using for`, constructors | -| Functions | external/public, nonpayable or view, ≤1 parameter (`uint256`/`address`), ≤1 unnamed `uint256` return, no modifiers, not virtual | everything else ("unsupported function surface") | -| Statements | `x = / += / -= e`, `uint256 x = e`, `if (a < b) revert E();`, terminal `return e` | loops, `unchecked`, `if/else`, calls, `emit`, `delete` | -| Expressions | locals, `msg.sender`, literals, state reads, mapping reads, checked `+`/`-` | `*`, `/`, comparisons outside guards, casts, calls, `address(this)` | - -Trust story (`TRUST_ASSUMPTIONS.md`): solc's typed AST/storage layout and the -Lean translation are trusted; kernel checking establishes well-typed -definitions and theorems about their execution; storage keys are logical, -not keccak; `Contract.run` rolls back failed executions; no deployment, -calldata, gas, or bytecode claim. This roadmap keeps that story and only -widens the accepted fragment. - ---- - -## 3. Where we are going: the Pareto sources - -Inheritance chains at `54502d1`: - -``` -IdleCDOEpochVariant - is IdleCDOCreditVault - is PausableUpgradeable, (Initializable, ContextUpgradeable) - GuardedLaunchUpgradable (Initializable, OwnableUpgradeable, ReentrancyGuardUpgradeable) - IdleCDOStorage - -IdleCreditVault - is Initializable, OwnableUpgradeable, ERC20Upgradeable, ReentrancyGuardUpgradeable, IIdleCDOStrategy -``` - -`Initializable` and `ContextUpgradeable` are reached more than once in both -chains: these are real Solidity diamonds resolved by C3 linearization, so the -importer must accept them (unlike `verity_contract is`, which rejects diamonds; -gap G7). - -Files the compiler must see (all vendored, pinned by hash, Section 9): - -- idle-tranches: `IdleCDOCreditVault.sol`, `IdleCDOEpochVariant.sol`, - `strategies/idle/IdleCreditVault.sol`, `GuardedLaunchUpgradable.sol`, - `IdleCDOStorage.sol`, `IdleCDOTranche.sol`, and the interfaces - `IIdleCDOStrategy`, `IERC20Detailed`, `IProgrammableBorrower`, - `keyring/IKeyring`, plus `@uniswap/v2-periphery`'s `IUniswapV2Router02` - (only as the type of an opaque storage field). -- OpenZeppelin contracts-upgradeable 4.9.6: `Initializable`, - `ContextUpgradeable`, `OwnableUpgradeable`, `PausableUpgradeable`, - `ReentrancyGuardUpgradeable`, `ERC20Upgradeable` (+ `IERC20Upgradeable`, - `IERC20MetadataUpgradeable`), `SafeERC20Upgradeable` (+ - `AddressUpgradeable`, `IERC20PermitUpgradeable`). - -Construct inventory (from the three target contracts, their idle-tranches -parents, and the OZ parents), with the slice that owns each construct and the -`VERITY-GAPS.md` row it relates to: - -| Construct | Where it appears | Slice | Gap | -|-----------|------------------|-------|-----| -| `is A, B, C`, C3 linearization, diamonds, `abstract`, body-less `virtual` (`getContractValue`), `override`, `super._transfer` / `super._deposit` | all chains | S1 | G6, G7, G18 | -| internal function calls, internal helpers with several parameters, named return variables | everywhere (`_checkOnlyOwner`, `_virtualPriceAux`, …) | S1 | G13 | -| `uint256[N] __gap`, `address[]`, `bytes32`, `string`, interface-typed fields | OZ parents, `IdleCDOStorage`, `IdleCreditVault` | S1 (opaque fields) | G5, G17 | -| modifiers with prelude only (`onlyOwner`, `whenNotPaused`, `onlyInitializing`) and with postlude (`nonReentrant`, `initializer`) | OZ parents | S2 | G18 | -| `bool`, `uint8`, `address` scalars, packed sub-word fields (`_initialized`/`_initializing`, `isEpochRunning`/`allowAAWithdrawRequest`/…) | OZ, `IdleCDOStorage`, `IdleCDOEpochVariant` | S2 | G4 | -| structs in mappings (`Apr0UserData`), storage pointers to struct members, `delete`, nested mappings `mapping(address => mapping(uint256 => uint256))`, `mapping(uint256 => uint256)` | `IdleCreditVault` | S2 | G2 | -| constants (`FULL_ALLOC`, `YEAR = 365 days`, `10**18`, `type(uint256).max`), `require(cond, "…")`, `if/else`, ternary, `&&`, `\|\|`, `!`, all comparisons, checked `*`, `/`, `%`, `block.timestamp`, `address(this)`, `emit` | everywhere | S2 | G8, G9, G10 | -| typed interface calls on `IERC20Detailed(token)`, `IdleCreditVault(strategy)`, `IIdleCDOEpochVariant(idleCDO)`, `IKeyring`, `IProgrammableBorrower`; multi-value external returns | `IdleCDO*`, `IdleCreditVault` | S3 | G1, G5, G14, G15 | -| `using SafeERC20Upgradeable for …`: `safeTransfer`, `safeTransferFrom`, `safeIncreaseAllowance` | `IdleCDOCreditVault`, `GuardedLaunchUpgradable`, `IdleCreditVault` | S3 | — | -| `try this.f(x) { … } catch { … }` (three sites, all bare `catch`, all self-calls) | `IdleCDOEpochVariant.startEpoch`, `_stopEpoch`, `getInstantWithdrawFunds` | S3 | G3 | -| `int256` storage (`interestForOverUnderPerformance`), locals, tuple returns `(uint256, int256)`, `int256(x)`/`uint256(y)`, unary `-`, signed compare, signed checked arithmetic | `_virtualPriceAux`, `_updateAccounting`, `_calcInterestWithdrawRequest`, `IdleCreditVault` | S4 | G9 | -| `pragma solidity 0.8.10;`, `^0.8.0` / `^0.8.2` in OZ files, `import` directives, remappings, multi-file source units | all | S5 | — | -| `initialize` (`new IdleCDOTranche`, `string` concatenation, `abi.encodePacked`, `10**decimals()`), constructors, function-typed locals (`_getAARatio`), `tx.origin` | `IdleCDOCreditVault`, `IdleCreditVault` | out of scope: excluded entry points (S6) | — | - -Two facts shape the design more than any other: - -- **solc already flattens.** The `storageLayout` output for a most-derived - contract lists every inherited variable with its final slot and offset, and - `linearizedBaseContracts` gives the C3 order. The importer never computes - an inheritance layout itself; this is why gap G7 (absolute slots that - cannot serve two chains) does not exist on the import route. -- **The importer owns its AST.** Gaps G8–G10, G12, G13 and G16 are - limitations of the `verity_contract` surface syntax. The imported term is - built in `MetaM` from solc declaration ids; Lean identifiers, keyword - clashes, and definition order are not constraints. Gaps that concern the - *executable core* (G1, G2, G4, G14) do apply and are addressed in S2/S3. - ---- - -## 4. Design decisions that span slices - -### 4.1 Program shape: callee-first function table - -`Syntax.lean` gains a function context in the same de Bruijn style as `Ctx` -and `Layout`: - -- `Sig := (params : Ctx) × (rets : List Ty)`; `Fns := List Sig`. -- `FVar : Fns → Sig → Type` indexes an already-registered function. -- `Expr`/`Stmt` become `Expr L F Γ t` / `Stmt L F Γ r`; `Expr.call : - FVar F σ → Args L F Γ σ.params → …`. - -Each Solidity function (internal or external, from any contract in the -linearization) is registered as its own Lean constant in callee-first order. -`Fn.meaning body slots fns env` takes an `FnEnv F` term built from the -already-registered constants (the same pattern as `slotsTerm`). Consequences: - -- Recursion (direct or mutual) is unrepresentable and is rejected by the - parser with a diagnostic naming the cycle. The Pareto sources are expected - to be recursion-free; S1's acceptance run on the vendored sources confirms - it (a hit would be a scoped rejection of that function, not a design - change). -- `#print Child.bump` shows the call to `Base._bump` by constant name, so the - "parsed AST under its meaning" readability property of the POC is kept. -- Kernel terms stay per-function sized; nothing grows with the size of the - whole contract except `step`. - -### 4.2 Storage: solc layout is the only layout - -Every entry in solc's `storageLayout.storage` is either an **imported field** -(type in the accepted set, gets a `Slot` handle, a `Storage` member and, -if public, a getter) or an **opaque field** (any other type: `__gap` arrays, -`bytes32`, `string`, `address[]`, interface-typed fields). Opaque fields keep -their slot reserved, appear in an `opaqueFields` audit constant, and are not -in `Storage`; any body that reads or writes one is rejected at that -statement. This replaces the POC's "unaccounted layout field" rejection and -is the mechanism that lets each slice widen the type set independently. - -Sub-word (packed) fields and struct members are read and written on the -word at their slot with shift/mask (S2). Nested mappings and struct members -inside mappings derive a **logical** inner base with a provably injective -`Nat` pairing offset by `2^256` so it can never equal a solc slot (S2). -This is consistent with the existing "logical keys, not keccak" trust -stance and needs no axiom. - -### 4.3 External behaviour is a parameter, never an axiom - -Imported definitions that perform external calls take an explicit -`world : Sol.CallModel` argument (S3). `CallModel` has a `call` field for -state-changing calls and a `staticCall` field that cannot write. Theorems -quantify over `world` and state their assumptions as hypotheses; the S6 -smoke and later work may instantiate `world` with the imported strategy's -own meaning through `Verity.MultiContract` hops. `try this.f()` is -`Contract.selfCall` under `Contract.tryWith` and does not touch `world`. - -### 4.4 Entry points and `step` - -`step` stays the disjunction of every public/external function and public -getter, in a fixed order: the target contract's functions in source order, -then each base in linearization order, then getters in layout order. S6 -adds an explicit entry-point allowlist for partial imports; the excluded -functions are recorded in a registered `unmodeledEntryPoints` constant that -the trust docs and the smoke's spec must cite. - -### 4.5 Registered sources - -`registeredSource : String` becomes a hard-coded manifest -`registeredSources : List (String × String)` (logical path, package-relative -path) in `Importer.lean`, still part of the digest, still checked for -canonical package containment. The lakefile tracks each smoke directory with -`input_dir` plus a `lean_lib` with `needs`, mirroring the `VaultFromSolidity` -target. - ---- - -## 5. Slice S1: inheritance - -**Goal**: import a most-derived contract whose bases live in the same source -unit, with solc's linearization, flattened layout, virtual dispatch, `super`, -abstract bases, internal function calls, and opaque storage fields. - -### Files - -- `Contracts/VaultFromSolidity/Importer/Syntax.lean`: `Sig`, `Fns`, `FVar`, - `Args`; index `Expr`/`Stmt` by `F`; `Expr.call`, `Stmt.callStmt`; - `Ret` generalized to `List Ty` (parser still accepts 0 or 1 until S3). -- `Contracts/VaultFromSolidity/Importer/Semantics.lean`: `FnEnv`, - `Fn.meaning` with `fns`; call meaning in Solidity evaluation order - (arguments left to right, then callee body under the caller's - `msg.sender`/`msg.value`). -- `Contracts/VaultFromSolidity/Importer/Importer.lean`: - - accept several `ContractDefinition`s; `solidity_contract A from "f.sol" - contract "Name"` selects the target (defaults to the sole non-abstract - contract); - - accept `abstract`, `baseContracts`, `InheritanceSpecifier`, - `linearizedBaseContracts` with more than one id, `contractDependencies` - (must reference contracts in the unit), body-less `virtual` functions - whose signature is implemented in the linearization (`fullyImplemented` - of the target must be true), `override`/`overrides` metadata and - `baseFunctions`; - - virtual dispatch and `super` resolved by signature over the target's - `linearizedBaseContracts` (most-derived first for calls; next-after-the - -defining-contract for `super`), cross-checked against the AST's - `referencedDeclaration`/`baseFunctions` and rejected on disagreement; - - storage entries matched by `astId` to the declaring contract in the - linearization instead of `logicalPath:contractName`; opaque fields; - - callee-first registration order with cycle rejection; `_` - -prefixed Lean names for base functions to avoid collisions; - - `step` order per Section 4.4; - - `registeredSources` manifest. -- `Contracts/SolidityImportSmoke/Inheritance/{Inheritance.sol, - Inheritance.lean, Spec.lean, Proofs.lean}`: synthetic `Storage`-like base, - `Pausable`-like base with `internal virtual _pause`, `Ownable`-like base, - child overriding `_pause` and calling `super._pause()`, one diamond - (`Base` reached twice). Only POC types plus `address` scalars. -- `Contracts/SolidityImportSmoke/Inheritance/scripts/inheritance_test.py` - (same disposable-package pattern as `solidity_importer_test.py`). -- `lakefile.lean`, `scripts/check_contract_structure.py`, docs per rule 6. - -### Tests - -- Acceptance suite runs the full POC suite unchanged, then: - - builds the synthetic smoke; probes slot numbers of inherited fields - against solc's layout (`[base slots…, child slots…]`); - - `#print Child.go` shows `Expr.call` to `Child__pause` and - `#print Child__pause` shows the `super` hop to `PausableLike__pause`; - - mutation: swapping the `is` order changes the linearization and the slot - probe; changing the override body breaks the dispatch theorem; adding a - public function to a base adds a `step` disjunct and breaks the - invariant theorem; importer/syntax/semantics content edits move the - digest; - - rejections with positions: direct and mutual recursion, unimplemented - virtual in a non-abstract target, a body touching an opaque field, - `is` referencing a contract from another file (until S5), function - overloading by parameter type (same name, different signature: rejected - in v1 to keep name resolution simple). -- Proofs (no `sorry`): `dispatch_is_child` (an entry point in the base that - calls the virtual observes the child's override), `super_runs_parent`, - `paused_invariant : PreservedBy … step`, and one `*_meets_spec` per entry - point. - -### Acceptance - -- `lake build VaultFromSolidity SolidityImportSmokeInheritance` green; both - python suites pass; axiom audit reports only `propext`, `Quot.sound` for - every theorem in both proof files. -- Dry run on the vendored Pareto sources is **not** required here (they - need S2–S5), but the slice records in its PR the list of Pareto functions - that are recursion-free (expected: all). - -### Non-goals - -Modifiers, events, constructors, initializers, multi-file units, packed or -non-word storage, external calls, `this.`, overloading, function-typed -values, libraries, `using for`. - ---- - -## 6. Slice S2: modifiers, structs, and the declaration surface - -**Goal**: the declaration and statement surface that OpenZeppelin parents -and the Pareto bodies use, without external calls or signed integers. May -land as three PRs in this order: S2a modifiers and events, S2b storage -shapes, S2c statements and expressions. One roadmap slice, one acceptance -suite. - -### Design - -- **Modifiers** are inlined at parse time; `Semantics.lean` never sees `_`. - A function with modifiers `m1 m2` and body `B` is parsed as - `m1-pre; (m2-pre; let r ← block B; m2-post); m1-post; return r`. A - `block` is a new `Stmt` form whose `return` exits the block only, so - `nonReentrant`'s postlude runs after an early `return` exactly as in - Solidity, and a `revert` anywhere still aborts the whole call. Modifier - bodies may call internal functions (S1 machinery); a modifier with more - than one `_`, or with arguments (`reinitializer(uint8)`), is rejected. -- **Events** parse to `Stmt.emit name args` whose meaning is - `Verity.emitEvent`; events are part of the state and available to specs - but no slice proves anything about them. -- **Storage shapes**: `bool`, `uint8`, `address`, `int256` (tag only; S4 - gives it arithmetic) scalars at any `offset` with `numberOfBytes` 1/20/32, - read as shift-and-mask on the word and written by masked merge; - `mapping(uint256 => uint256)`, `mapping(address => mapping(uint256 => - uint256))`, `mapping(address => Struct)` with word-sized members, storage - pointer locals `S storage p = m[k]`, member read/write, `delete m[k]` - (writes every member to zero). Logical inner bases per Section 4.2. - `Storage` exposes `apr0Users : Address → Apr0UserData` as a Lean structure. -- **Constants** (`constant` state variables with literal or - literal-arithmetic initializers, `365 days`, `10**18`, `type(uint256).max`) - are evaluated at import time to `Nat` literals; the evaluator is part of - the digest and rejects non-literal initializers. -- **Statements/expressions**: `if/else`, nested blocks, `bool` locals, - ternary, `&&`/`||` with short circuit (both operands are pure in the - accepted subset, so evaluation order is not observable; the parser rejects - operands with calls until S3 decides), `!`, `==`/`!=`/`<`/`<=`/`>`/`>=` on - `uint256`, `address`, `bool`; checked `*`, `/`, `%` (`Panic(0x11)`, - `Panic(0x12)`) via `Verity.Stdlib.Math`; `require(cond, "…")` with the - string kept verbatim; `revert E(args)` with word-sized arguments; - `block.timestamp`, `address(this)`; assignment to locals and named return - variables; `return;` in unit functions. - -### Files - -- `Syntax.lean`, `Semantics.lean`, `Importer.lean` as above; new allowed - node kinds: `ModifierDefinition`, `ModifierInvocation`, - `PlaceholderStatement`, `EventDefinition`, `EmitStatement`, - `StructDefinition`, `UserDefinedTypeName`, `UnaryOperation`, - `Conditional`, `TupleExpression` (parenthesized only), `MemberAccess` on - structs, `ElementaryTypeNameExpression` (for `type(uint256).max`), `Literal` - with `subdenomination`. -- `Verity/Core.lean` or `Verity/Core/Semantics.lean`: word-level packed - read/write helpers and their `solidity_simp` lemmas if not already - exported (`compiledPackedRead` exists on the compiler side; the executable - core needs the same two functions with proofs `packed_read_write`, - `packed_write_other_field`). No `ContractState` field is added. -- `Verity/Proofs/Stdlib/SolidityImport.lean`: `solidity_simp` extended with - the new primitives and the pairing lemmas. -- `Contracts/SolidityImportSmoke/Modifiers/…` (OZ-shaped `Ownable`, - `Pausable`, `ReentrancyGuard`, `Initializable` re-typed in one file with - `pragma ^0.8.33`, plus a child using `nonReentrant` with an early return), - `Contracts/SolidityImportSmoke/Storage/…` (packed bools at the same slot, - a struct mapping, a nested mapping, `delete`), each with `Spec.lean`, - `Proofs.lean`, and a python acceptance script. - -### Tests - -- Modifier order mutation (swap `nonReentrant whenNotPaused`) breaks a - theorem that pins the revert reason order; dropping a postlude breaks - `status_restored`; a `return` before `_` in the synthetic body still - restores `_status` (theorem, not just a test). -- Packed layout: solc's `offset`/`numberOfBytes` drive the mask; mutation - reordering two bools in one slot changes the probe and keeps proofs - passing (names bind to solc offsets, as slots do today). -- Struct/nested mapping: `set_then_get` and frame theorems - (`other_key_unchanged`, `other_member_unchanged`) proved from the pairing - injectivity lemma; `#print axioms` on that lemma shows none. -- Rejections: modifier with two `_`, modifier arguments, struct with a - dynamic member, `mapping` with struct key, `unchecked`, loops, - inline assembly, `string` locals, arrays, `delete` on a scalar of opaque - type, `require` without a message is *accepted* (empty reason). - -### Acceptance - -- All prior suites green; new smokes build; axiom audit clean. -- Dry run: the importer applied to the vendored OZ parents alone (with the - OZ pragmas rewritten to `^0.8.33` in the smoke copy) accepts - `OwnableUpgradeable`, `PausableUpgradeable`, `ReentrancyGuardUpgradeable`, - `Initializable` bodies except those that call `AddressUpgradeable` - (`initializer`'s `isContract` check, which lands in S3 as a static call - primitive) and `_msgData` (`bytes calldata`, out of scope; never called by - the Pareto sources). - -### Non-goals - -External calls, libraries, `try`, signed arithmetic, dynamic arrays, -strings and `bytes`, `enum`, parameterized modifiers, `unchecked`, loops, -events in specs, gas. - ---- - -## 7. Slice S3: external calls and try/catch - -**Goal**: typed interface calls, the three OpenZeppelin SafeERC20 helpers -the Pareto sources use, multi-value returns, and `try this.f(args) {…} -catch {…}`, with callee behaviour as an explicit model parameter. - -### Design - -- `Sol.CallModel` in `Semantics.lean`: - `call : Address → Selector → List Uint256 → Contract (List Uint256)` and - `staticCall : Address → Selector → List Uint256 → ContractState → - Except String (List Uint256)`. Selectors are computed from the interface - signature with the existing kernel-computable keccak engine (already used - for `functionSelector` checks elsewhere in the repo), so a `CallModel` - instantiation can be keyed by real ABI selectors later. -- Interface calls: `I(expr).f(args)` where `I` is an `interface` (or a - contract used as an interface, as in `IdleCreditVault(strategy)`) parse to - `Expr.extCall`/`Stmt.extCallStmt` with the target address expression, - selector, word-typed arguments, and word-typed returns (0..n). `view` - interface methods route to `staticCall`; a `view` importing function may - only contain static calls (checked from `stateMutability`). -- Library helpers: `using SafeERC20Upgradeable for …` plus - `token.safeTransfer(to, v)`, `safeTransferFrom(from, to, v)`, - `safeIncreaseAllowance(spender, v)` parse to `Expr.libCall` over a closed - `LibFn` enumeration. Their meaning is written directly in `Semantics.lean` - as the ERC-20 call through `world.call` plus the OZ optional-return check - (`_callOptionalReturn`: revert unless returndata is empty or decodes to - `true`); `safeIncreaseAllowance` is `allowance` static read, checked add, - `approve`. The library bodies themselves (assembly, `abi.encodeWithSelector`) - are never imported; this is a documented trusted translation of three - functions, pinned to the OZ 4.9.6 source hash. -- `try this.f(args) { S } catch { C }`: `f` must be an external function of - the same contract; meaning is `Contract.tryWith (Contract.selfCall (f args - with sender := address(this), value := 0)) (fun _ => S) C`. Only bare - `catch` is accepted (all three Pareto sites are bare). Returndata is not - bound. -- Multi-value returns: `(uint256 a, uint256 b) = e;` and - `(a, b) = e;` for internal and external calls; named return variables of - any arity. -- `address(this)` compares and `msg.sender != address(this)` guards are S2 - expressions; `IERC20Detailed(_token).balanceOf(address(this))` is the - canonical static-call test. - -### Files - -- `Syntax.lean`, `Semantics.lean` (`CallModel`, `extCall`, `libCall`, - `tryCatch`, tuple binding), `Importer.lean` (new nodes: - `TryStatement`, `TryCatchClause`, `FunctionCall` with `tryCall = true`, - `UsingForDirective`, `InterfaceDefinition` / `contractKind = interface`, - `FunctionCall` of kind `typeConversion` for `I(addr)` and `address(x)`, - `MemberAccess` on interface-typed expressions, `Identifier` `this`). -- `Verity/Core.lean`: lemmas `tryWith_success`, `tryWith_revert_rollback`, - `selfCall_sender` if missing; added to `solidity_simp`. -- `Contracts/SolidityImportSmoke/Calls/…`: an interface, a token param, - `safeTransfer`, a static read, a self-call `try` with a success path and a - revert path (`sendFunds` reverts when `amount > balance`), a two-value - external return. - -### Tests - -- Theorems: `catch_iff_selfcall_reverts`, `catch_state_is_snapshot` - (post-state of the catch branch equals the pre-`try` state plus the - handler's writes), `success_path_writes`, `static_call_does_not_write`, - `safeTransfer_reverts_on_false` (for any `world` returning `false`), - all universally quantified over `world`. -- Mutations: remove the handler write; make `sendFunds` revert - unconditionally; swap argument order in an interface call (selector and - argument list change, theorem breaks); change the OZ helper hash pin - (import fails). -- Rejections: `try` on a non-self target, `catch Error(string)`, - `catch Panic`, `.call{value: …}`, `delegatecall`, `payable`, library - functions outside the closed set, interface methods with dynamic - arguments or returns (`bytes`, `string`, arrays), `new`. - -### Acceptance - -- All suites green; axiom audit clean (`CallModel` is a binder, so - `#print axioms` is unchanged by design). -- Dry run on the vendored `GuardedLaunchUpgradable` and `IdleCDOStorage` - (pragmas rewritten) accepts every function except `_deployTranche`-style - code, and the S2 leftover `initializer` now accepts `isContract` as a - static primitive (`extcodesize` modelled as a `staticCall` on a reserved - selector in `CallModel`). - -### Non-goals - -Modeled callees inside the importer (instantiating `world` with another -imported contract is proof-side work after S6), ETH value, reentrancy -adversary registries, returndata in `catch`, dynamic ABI, events in specs. - ---- - -## 8. Slice S4: signed integers - -**Goal**: `int256` as a first-class accepted type with Solidity 0.8 checked -semantics, enough for `_virtualPriceAux`, `_updateAccounting` and -`_calcInterestWithdrawRequest`. - -### Design - -- `Ty.int` denotes `Verity.Core.Int256`. Storage scalars (`FieldType.int256` - already exists in the layout reports), locals, parameters, single and - tuple returns. -- Literals: non-negative literals typed `int256` by solc; unary minus on - any `int256` expression is checked negation (`Panic(0x11)` on `minValue`). -- Conversions `int256(u)` / `uint256(i)` are bit reinterpretation - (`Int256.ofUint256` / `toUint256`, no range check) exactly as Solidity - 0.8 defines explicit conversion between same-width signed/unsigned types. -- Checked `+ - * /` and `%` via `Int256.safeAdd/safeSub/safeMul/safeDiv/ - safeMod` (`Panic(0x11)` overflow, `Panic(0x12)` division by zero; `minValue - / -1` is `Panic(0x11)`); `-=`/`+=`/`*=`/`/=` on `int256` lvalues; - comparisons via `slt/sgt/sle/sge`; ternary on `int256`. -- `solidity_simp` gains the `Verity/Proofs/Stdlib/Int256.lean` bridge - lemmas (wrapping equals unbounded `Int` on success). PR #2414 removes - `native_decide` from the sign-bit lemmas; S4 depends on it being merged - and its acceptance suite re-checks that every lemma it uses reports no - `Lean.ofReduceBool`. - -### Files - -- `Syntax.lean`, `Semantics.lean`, `Importer.lean` (type strings - `int256`, `t_int256`, `t_rational_*` literal typing, `UnaryOperation` `-`, - `FunctionCall` kind `typeConversion` between `int256`/`uint256`). -- `Verity/Proofs/Stdlib/SolidityImport.lean`: signed lemma set. -- `Contracts/SolidityImportSmoke/Signed/…`: a re-typed `_virtualPriceAux` - (`gain = int(nav) - int(lastNav)`, fee split, `max(gain, -int(lastNavBB))` - waterfall, `uint256(int256(x) + gain)`), with `pragma ^0.8.33`. - -### Tests - -- Theorems: `bb_absorbs_loss_first`, `aa_never_below_zero_when_bb_covers`, - `conversion_roundtrip`, overflow premises stated as `Int` inequalities; - `gain_split_sum` (AA gain + BB gain = total gain after fee, as `Int`). -- Mutations: flip a `>` to `>=` in the waterfall; drop the fee term; replace - checked `-` by unchecked wrap (via `unchecked` would be rejected, so the - mutation edits the *spec* instead, as the POC does for Spec.lean). -- Rejections: `int128`/narrow signed types, `>>`/`<<` on `int256`, `int256` - mapping keys, implicit conversions solc would reject anyway (fixture - proves the diagnostic is ours, not solc's). - -### Acceptance - -- All suites green; axiom audit clean; no `native_decide` in any lemma - reachable from the smoke (checked by the suite through `#print axioms` - rejecting `Lean.ofReduceBool`). -- Dry run: the importer accepts `IdleCDOCreditVault._virtualPriceAux`, - `_updateAccounting` and `IdleCDOEpochVariant._calcInterestWithdrawRequest` - bodies from the vendored sources (pragmas rewritten). - -### Non-goals - -`sar`/shifts, narrow signed widths, signed `**`, `int256` in ABI-dynamic -positions, signed constants beyond literals. - ---- - -## 9. Slice S5: solc 0.8.10 and multi-file source units - -**Goal**: compile the Pareto sources unmodified (`pragma solidity 0.8.10;`) -through a second pinned compiler, with imports and remappings resolved -entirely from a vendored, hash-pinned manifest. - -### Findings that fix the design (verified against the real binaries) - -| | solc 0.8.33 (current pin) | solc 0.8.10 | -|-|---------------------------|-------------| -| Binary | `solc-linux-amd64-v0.8.33+commit.64118f21`, sha256 `1274e5c4…5468` | `solc-linux-amd64-v0.8.10+commit.fc410830`, sha256 `c7effacf28b9d64495f81b75228fbf4266ac0ec87e8f1adc489ddd8a4dd06d89` | -| `--no-import-callback` | supported | **absent** (added in 0.8.22) | -| `evmVersion: cancun` | supported | **rejected** (0.8.10 tops out at `london`) | -| AST fields missing in 0.8.10 on the POC Vault | — | `ContractDefinition.usedEvents`, `ErrorDefinition.errorSelector`, `FunctionCall.nameLocations`, `MemberAccess.memberLocation`, `Mapping.keyName/keyNameLocation/valueName/valueNameLocation` | -| Pragma literals | `["solidity","^","0.8",".33"]` | `["solidity","0.8",".10"]` (exact); OZ files use `^0.8.0` / `^0.8.2` | - -### Design - -- **Compiler table**: `Importer.lean` carries a closed table - `{version output string, sha256, evmVersion, supportsNoImportCallback}` - for 0.8.33 and 0.8.10. The pragma of the *target* file selects the - compiler; every file in the manifest must have a pragma satisfied by that - version (exact `0.8.10` or a caret range containing it). The chosen - compiler's sha256 goes into the digest. -- **Fail-closed imports without `--no-import-callback`**: every source is - supplied inline in standard JSON; `settings.remappings` maps - `@openzeppelin/contracts-upgradeable/` and `@uniswap/v2-periphery/` onto - logical paths in the same JSON; solc runs with `cwd` set to an empty - temporary directory and no `--base-path`/`--allow-paths`; the output - `sources` keys must equal the manifest exactly and `ImportDirective. - absolutePath` values must all be manifest keys. Any file-callback - resolution therefore fails. -- **Version-indexed schema**: `allowedNodeFields`/`requiredNodeFields` - become functions of the compiler entry; the differences above are the - initial delta. The first task of the slice is a committed fixture - (`Importer/fixtures/ast-fields-0.8.10.txt`, generated by a script that - walks the AST of the vendored Pareto sources) diffed against the 0.8.33 - fixture; every difference is either allowed with a meaning or rejected. -- **Multi-file**: `ImportDirective` (plain and `{X} from`), several - `SourceUnit`s, `exportedSymbols` across files, `linearizedBaseContracts` - and `referencedDeclaration` ids resolving across files, storage entries' - `contract` field naming another file. Source positions in diagnostics - carry the logical path of the right file. -- **Vendoring**: `Contracts/ParetoFromSolidity/vendor/idle-tranches/…` at - `54502d1` and `…/vendor/openzeppelin-contracts-upgradeable/…` at - v4.9.6, byte-identical, with `vendor/MANIFEST.json` (upstream URL, - commit/tag, path, sha256 per file) checked by a script and the same - hashes recorded in the pareto closure repo's `audit/source-map.yaml`. - Licenses: idle-tranches files are `UNLICENSED`/Apache-2.0 per header, OZ - is MIT; the vendor directory carries both notices. -- **CI**: `.github/actions/setup-solc` gains a second version triple - (`SOLC_IMPORT_LEGACY_VERSION/URL/SHA256`) installed to - `.lake/solidity-import/solc-0.8.10`; `scripts/check_solc_pin.py` checks - both; `Makefile` target `test-solidity-import` runs every importer suite. - Runner architecture must be confirmed (this workspace reports `x86_64`; - official 0.8.10 binaries exist for `linux-amd64` only). - -### Files - -- `Importer.lean` (compiler table, manifest, remappings, multi-file - resolution, version-indexed schema), `Importer/fixtures/*`, - `Importer/scripts/ast_fields_fixture.py`. -- `Contracts/SolidityImportSmoke/Vault810/{Vault.sol, …}`: the POC Vault - with `pragma solidity 0.8.10;` and the same `Spec.lean` and - `ExecutionProof.lean` (copied, not shared), proving the seven theorems - under the other compiler. -- `Contracts/SolidityImportSmoke/MultiFile/…`: a child importing a base - from a second file through a remapping. -- `.github/actions/setup-solc/action.yml`, `.github/workflows/verify.yml`, - `scripts/check_solc_pin.py`, `Makefile`, `TRUST_ASSUMPTIONS.md` (two pins). - -### Tests - -- Compiler substitution: 0.8.33 binary for a `0.8.10` target → "compiler - version mismatch"; tampered 0.8.10 binary → "compiler checksum mismatch"; - a manifest file missing → solc error surfaced with position; an import - resolving outside the manifest (probe writes a file into the temp cwd) → - "unexpected compiler sources"; a pragma outside the table → rejected. -- Schema: the synthetic AST wrapper probes from the POC re-run for 0.8.10 - (`ast`, `metadata`, `typed`, `missing`, `span`, `layout`). -- Seven Vault theorems pass under 0.8.10; slot probe identical to 0.8.33. - -### Acceptance - -- All suites green under both pins; axiom audit clean. -- The vendored Pareto sources compile through the importer's solc - invocation with no solc errors, and the AST-field fixture for 0.8.10 is - committed and fully classified (every field allowed or rejected, none - "unvalidated"). - -### Non-goals - -Any 0.8.x other than 0.8.10 and 0.8.33; `viaIR`; optimizer settings; -`hardhat`/`foundry` build parity; on-chain bytecode matching. - ---- - -## 10. Slice S6: one Pareto smoke - -**Goal**: import `IdleCDOEpochVariant` (most-derived, full chain, unmodified -vendored sources, solc 0.8.10) as a **partial import** with an explicit -entry-point allowlist, and prove one contract-level property about a real -entry point. - -### Smoke selection - -Entry point: `IdleCDOEpochVariant.getInstantWithdrawFunds()`. It exercises -every slice at once: - -- S1: `_checkOnlyOwnerOrManager` (child) → `owner()` (OwnableUpgradeable, - via GuardedLaunchUpgradable) and `IdleCreditVault(strategy).manager()`; - `_handleBorrowerDefault` → `_pause()` (PausableUpgradeable, `internal - virtual`). -- S2: packed bools `isEpochRunning`, `allowInstantWithdraw`, `defaulted`; - `whenNotPaused` on `_pause`; `emit BorrowerDefault(funds)`; `NotAllowed` - custom error through `_checkNotAllowed`. -- S3: `_pendingInstant()` static call to the strategy, `try - this.getFundsFromBorrower(_instantWithdraws)` with `safeTransferFrom` - inside, `_strategy.collectInstantWithdrawFunds(...)` in the success path. -- S4: not on this path; covered by the additional view entry point - `virtualPrice(address)` (→ `_virtualPriceAux`, static calls to the - strategy and the token) which the allowlist also includes so the signed - path is imported from the real source. -- S5: real pragmas, real OZ files, real remappings. - -Property (spec sketch, stated over `view` and a universally quantified -`world`): - -- if the self-call reverts under `world`, the post-state has `defaulted = - true`, `allowInstantWithdraw` unchanged, the epoch contract paused, and - `world.call` was never invoked with the strategy's - `collectInstantWithdrawFunds` selector (stated as a trace predicate on the - `CallModel` log, which S3's `CallModel` records in `ContractState` - returndata-style journal or as an explicit log field of the model, to be - decided in S3 and reused here); -- if it succeeds, `allowInstantWithdraw = true` and the strategy was called - exactly once with the `_pendingInstant()` value. - -Candidate properties beyond this one are listed in the closure repo's -`audit/PROPERTIES.md`; only the two above are in scope. - -### Partial import mechanism - -`solidity_contract IdleCDOEpochVariant from "vendor/idle-tranches/contracts/ -IdleCDOEpochVariant.sol" contract "IdleCDOEpochVariant" entry_points -[getInstantWithdrawFunds, virtualPrice, getFundsFromBorrower]`: - -- every function reachable from the allowlist (internal calls, modifiers, - `super`, `this.` targets) must be fully accepted; anything else in the - contract is not parsed, is listed in the registered - `unmodeledEntryPoints : List String`, and is omitted from `step`; -- getters stay in `step` (they are cheap and needed for the views); -- the trust docs state that `step` covers the allowlist only, and the - smoke's `Spec.lean` header repeats the list. - -This is what makes `initialize` (`new IdleCDOTranche`, `string` -concatenation, `abi.encodePacked`), `_getAARatio`'s function-typed local, -and `IdleCreditVault`-side code stay out without weakening any rule in -Section 1. The alternative, a full import of every entry point, was rejected -for this roadmap: it requires contract creation, `string`/`bytes` memory, -and function-typed values, none of which serve the property. - -### Files - -- `Contracts/ParetoFromSolidity/vendor/…` (S5), `ParetoFromSolidity.lean`, - `Spec.lean`, `Proofs/InstantWithdrawSmoke.lean`, - `scripts/pareto_smoke_test.py`, `lakefile.lean` target - `ParetoFromSolidity`, `docs/VERIFICATION_STATUS.md` row, `README.md` - table row, `AUDIT.md` evidence entry. - -### Tests - -- The smoke script: builds `ParetoFromSolidity`; axiom audit; `#print` - probes that `getInstantWithdrawFunds` unfolds through `Stmt.tryCatch`, - `Expr.libCall .safeTransferFrom`, and `PausableUpgradeable__pause`; - vendored-hash check against `vendor/MANIFEST.json` and against upstream - `54502d1` (network-free: hashes are committed; a separate manual script - re-derives them from the pinned commit); -- mutations on a disposable copy of the vendored source: remove - `defaulted = true` from `_handleBorrowerDefault` → theorem breaks; move - `allowInstantWithdraw = true` into the `catch` → theorem breaks; add - an entry point to the allowlist that touches `initialize` → import - rejected with the position of the first unsupported node; -- unmodeled-entry-point manifest equals the expected list (any new - accepted function must be a deliberate change). - -### Acceptance - -- `lake build ParetoFromSolidity` green on `dgx-spark`; wall-clock and peak - memory recorded in the PR against the baseline in - [`docs/DGX_SPARK_BOTTLENECK_REPORT.md`](DGX_SPARK_BOTTLENECK_REPORT.md); - the kernel-check time of the largest imported function is reported. -- Axiom audit clean; every theorem under 30 lines or allowlisted. -- `TRUST_ASSUMPTIONS.md` has a "Proof-only Pareto import" section listing - the allowlist, `unmodeledEntryPoints`, the three trusted SafeERC20 - translations, both compiler pins, and the `CallModel` parameterization. - -### Non-goals - -`IdleCreditVault` as a modeled callee (instantiating `world`), any -`initialize`/constructor path, `IdleCDOCreditVault` deposits, the full -`step` relation, cross-contract invariants (for example strategy/vault -accounting equalities from the closure repo), events in specs, gas, bytecode, -proxy upgrade safety, `keyring` policy semantics. - ---- - -## 11. Sequencing and dependencies - -``` -S1 inheritance ──► S2 modifiers/structs ──► S3 external calls/try ──► S4 signed ──► S5 solc 0.8.10 ──► S6 Pareto smoke - ▲ - PR #2414 (Int256 lemmas without native_decide) -``` - -- S2 may be split into S2a/S2b/S2c PRs; each keeps the S1 smoke green. -- S3 and S4 touch disjoint constructors and could be developed in parallel - branches, but merge in order (S3 first) to keep tuple-return handling in - one place. -- S5 can be prototyped early (the compiler-table and fixture work does not - depend on S1–S4) but merges after S4 so the fixture classification covers - the final node set. -- Relative size: S1 large, S2 large (three PRs), S3 medium, S4 small, - S5 medium, S6 medium. - ---- - -## 12. Risks and open questions - -| Risk | Impact | Mitigation / decision point | -|------|--------|-----------------------------| -| `super` annotation in solc's AST is relative to the defining contract, not the imported most-derived contract | wrong dispatch would be a silent semantic bug | S1 resolves `super` itself over the target's linearization and treats disagreement with the AST as a rejection; a synthetic diamond test pins it | -| Kernel-check time of large intrinsically typed bodies (`_stopEpoch` is ~190 lines) | S6 build time on `dgx-spark` | per-function constants (4.1); S6 records timings; if a body exceeds budget, split it via the allowlist rather than weakening checks | -| Packed-field semantics duplicated between compiler (`compiledPackedRead`) and the executable core | drift | S2 proves the two agree on word inputs (`width < 256` calculation, already the compiler's argument) | -| `CallModel` call log representation | S6 property needs "the strategy was called once" | decide in S3: explicit journal on the model side, never a `ContractState` field (EVMYulLean exhaustive matches) | -| Function-typed local in `_getAARatio` | blocks `getApr`/`getCurrentAARatio` | out of allowlist; if ever needed, a narrow rewrite (`cond ? f : g` used only in call position → `if` at each call site) is a separate slice | -| solc 0.8.10 has no official `linux-arm64` binary | CI on non-x86 runners | confirm runner architecture before S5; fallback is building solc from source with a pinned hash, documented as a trust surface | -| OZ `initializer` modifier uses `AddressUpgradeable.isContract` (`extcodesize`) | needed only if `initialize` were modeled | out of allowlist in S6; S3 models `isContract` as a reserved static primitive so the OZ file still parses | -| `require` reason strings and panic codes are model strings, not ABI revert bytes | unchanged from the POC | keep the existing trust wording; no slice claims revert-data equivalence | -| Upstream `54502d1` sources contain `UNLICENSED` headers | vendoring | vendor with the upstream notice, proof-only use, confirm with the closure repo owners before S5 merges | - ---- - -## 13. Gap coverage summary - -| Gap | Route on this roadmap | -|-----|-----------------------| -| G1 modeled callee | `CallModel` parameter (S3); instantiation with the imported strategy is post-S6 | -| G2 nested mappings / struct mappings are stubs | real logical keys via injective pairing (S2) | -| G3 `selfCall` with arguments | `try this.f(args)` (S3) | -| G4 packed `bool`/`address` fields | shift/mask on solc offsets (S2) | -| G5 interface-typed storage fields | opaque field (S1) + typed call on `I(addr)` expressions (S3) | -| G6 override cannot see child helpers | dispatch by linearization, no positional elaboration (S1) | -| G7 absolute slots, diamonds rejected | solc layout per chain, C3 accepted (S1) | -| G8–G10 statement/expression forms | importer-owned AST (S2) | -| G11 `view` frame theorem | not generated by the importer; `view` functions are ordinary definitions, static-call-only (S3) | -| G12, G13, G16 naming and ordering | non-issues: declaration ids, callee-first registration (S1) | -| G14 multi-value interface returns | tuple returns (S3) | -| G15 cyclic `linked_contracts` | `CallModel` needs no binding order (S3) | -| G17 gaps, `bytes32`, `string`, `uint8` storage | opaque fields (S1); `uint8` packed scalar (S2) | -| G18 modifiers calling helpers, postludes, abstract functions | inlined modifiers with blocks (S2), abstract bases (S1) | diff --git a/TRUST_ASSUMPTIONS.md b/docs/TRUST_ASSUMPTIONS.md similarity index 82% rename from TRUST_ASSUMPTIONS.md rename to docs/TRUST_ASSUMPTIONS.md index dee15d087b..1b63ef9c88 100644 --- a/TRUST_ASSUMPTIONS.md +++ b/docs/TRUST_ASSUMPTIONS.md @@ -2,129 +2,54 @@ This document states what Verity proves and what it still trusts. -## Proof-only Solidity Vault import - -This POC is separate from the verified compilation pipeline below. It trusts -pinned solc's typed AST/storage layout and the Lean translation in -`Contracts/VaultFromSolidity/Importer/Importer.lean` to preserve Solidity -meaning. Kernel checking establishes well-typed definitions and theorems about -their execution, not a Solidity-to-Verity equivalence theorem. `sourceDigest` -is provenance, not proof of correspondence. It hashes the compiler input, -standard-json output, Lean importer implementation, and the release identity -`0.8.33+commit.64118f21` (not the host binary hash or Darwin/Linux banner), so -Linux and macOS share one translation identity when they produce the same AST. -The host's fixed checksum utility is trusted for compiler-pin checks: -`/usr/bin/sha256sum` on Linux and `/usr/bin/shasum` on macOS. The on-disk -compiler must match a committed allowlist of official -`binaries.soliditylang.org` SHA-256 digests for linux-amd64 and macosx-amd64 -(the latter is a universal Mach-O). `make setup-solc-importer` fetches the -platform `list.json`, requires the published digest to equal that pin, then -downloads the listed build. `make check-solc-published` repeats the live -list.json check without installing. Lake elaboration never fetches. Linux CI -still installs and runs the linux-amd64 artifact. -The digest is checked before version inspection, immediately before compilation, -and again after compilation, so `PATH` substitution and persistent compiler -replacement fail closed. As with all local builds, a concurrently malicious -process with the builder's own filesystem privileges is outside the threat model. -It is not full build identity: transitive Verity semantics, Lean toolchain, and -Lake build policy are tracked separately by normal build dependencies, not this -digest. The recursive closed AST schema rejects unknown fields/node kinds and -contract `layout at`; semantically used type metadata and all storage-layout -records are checked explicitly. Canonical package containment is checked -independently of source registration. - -`Importer.lean` runs pinned solc itself with `--standard-json` and -`--no-import-callback`, parses the typed AST/storage layout, validates the -closed subset, resolves IDs/types/storage slots, and parses it into the closed, -intrinsically typed inductive in `Syntax.lean`. Each entry point is registered -as `Semantics.lean`'s `Fn.meaning` applied to that parsed term, so -`Semantics.lean` is the single definition of what each construct means and is -part of the digest. Declaration -registration disables asynchronous kernel checking inside the transaction, -restores the pre-import environment on failure, parses every body at its typed -return signature (an ill-typed body is unrepresentable), and registers safe -transparent definitions. The same -transaction elaborates `Storage` with the standard structure command (the one -use of that elaborator; kernel-checked, rolled back on failure), then -registers `view : ContractState → Storage` built from the imported -`Slot` handles, tags slot handles, getters, functions, and `view` into -the `solidity_import` simp set, and registers a deterministic entry-point -relation `step` (the target's public/external functions in source order, then -each base in linearization order, then public getters in field order). -`Storage`, `view`, and `step` are reserved Solidity names. Opaque fields are -listed in `opaqueFields`. The importer source manifest is `registeredSources`. The -frontend emits no generated Lean source and keeps -no serialized AST/model cache; the parsed term is a kernel-checked Lean value, -never serialized. `Semantics.lean` tags its definitions into the -`solidity_import` simp set, so `solidity_simp` unfolds `Fn.meaning` down to the -Verity primitives exactly as it did before the split. - -The accepted fragment covers the existing Vault, the S1 inheritance slice, and -the S2 modifiers/structs slice: full-width `uint256` scalars, `address` scalars, -address-to-uint256 mappings and public getters, straight-line reads/writes, -locals, checked addition/subtraction, comparison/custom-error guards (including -`address !=` for `onlyOwner`), same-file `is` bases with solc's C3 -linearization (including diamonds), virtual dispatch and `super` specialized at - import time from the target's `linearizedBaseContracts` (matching 0.8.x runtime; - the AST `referencedDeclaration` on `super` follows the defining contract and is - not the dispatch key on diamonds; `super` inside an inlined modifier starts - after that modifier's defining contract, not the function's), internal function - calls (`Expr.call` is - `view`/`pure` only; effectful internals are `Stmt.callStmt`, because legacy - codegen evaluates those calls before the other operand / `+=` old-read), - abstract bases with body-less `virtual`s, opaque storage fields (slot reserved, - not in `Storage`; a body that reads or writes one is rejected), argument-free - modifiers inlined at parse time in declaration order (`Stmt.seq` prelude plus - `Stmt.block` so a function `return` still runs the postlude; prelude `uint256` - locals scope over the inlined body and postlude; identifier calls in a - modifier body that resolve to a private or non-virtual helper stay bound to - the declaring contract even if a derived contract declares a same-name private - function; a modifier with - two `_`, with arguments, or with a prelude `return` before `_` is rejected; - `Semantics.lean` never sees `_`), named `address`/struct returns defaulting to - the zero address rather than `msg.sender`, and - user structs used as parameters, storage, and returns when they are the closed - `uint256` then `address` pair (members occupy consecutive solc slots; encode is - `Expr.pair` indexed by the solc struct id so same-shape structs stay distinct - in signatures; a public struct field registers one product getter; a storage - struct assignment binds the RHS pair once; named `Acc({...})` constructors - evaluate arguments in source order (pinned 0.8.33) then pack into the - `uint256 × address` member product — `Expr.pair` when names are already - member order, `Expr.pairRev` when they are `{who, amount}`; positional - `Acc(a, b)` stays AST/member order). Unknown executable constructs are -rejected; this is not general Solidity support. Multi-file units, packed -fields, events, and external calls remain out of the fragment. -Arguments/context are already typed and decoded. `Contract.run` rolls back -failed executions; errors are model strings, not verified ABI revert bytes. -The storage model uses logical keys, not a proof of physical keccak layout. -There is no deployment, calldata/dispatch, gas, external interaction, bytecode, -or full EVM equivalence claim. Initial states are arbitrary, not proven deployed -states. Arithmetic success premises restrict the success theorems. The example keeps one -readable proof set: each successful entry point meets its named-storage spec -(`*_success_spec`), each spec holds under its precondition (`*_meets_spec`), -and the vault's solvency invariant (`totalAssets = totalSupply`) is preserved -by `step` (`solvent_invariant`). Rollback on revert is proved by `run_snd_cases`; -the revert conditions themselves are exercised by the acceptance suite, not proved. - -The specification states its promises over the imported storage view -(`v.totalAssets`, `v.shareBalances account`) rather than raw slot numbers; the -execution proof file relates that view to the imported definitions. Each -`*_meets_spec` theorem asserts that the call succeeds under its precondition -and that the successful post-state (or returned value) meets the spec, so a -reverting implementation cannot satisfy it; the `*_success_spec` theorems pin -the same named-storage equations from a success hypothesis. The view adds no -trust: `view` unfolds to `ContractState.readSlot`/`readMap` at the slot -solc's storage layout assigned. Zero-argument custom errors use Verity's `Name()` model convention; -arithmetic panic strings remain a model representation, not an assertion of -matching EVM revert bytes. The statements do not assert full equivalence of all -executions or all public/deployment interfaces. - -Lake's dedicated `VaultFromSolidity` target tracks source/compiler/Lean-importer/build -policy bytes and normal Lean dependencies. Acceptance evidence is obtained with -`python3 Contracts/VaultFromSolidity/Importer/scripts/solidity_importer_test.py`; -that Python file only orchestrates disposable builds and mutations and is not in -the translation path. Stale editor snapshots are not a current-source proof -certificate. No additional project axiom is introduced. +## Solidity import + +`Compiler/SolidityImport/Import.lean` selects functions from a solc project, +closes over the definitions solc's declaration ids actually reach, and elaborates +a `CompilationModel` (see `docs/SOLIDITY_IMPORT.md`). Execution of an accepted import is +`Compiler.CompilationModel.Denote.execStmt`, restricted by +`stmtListCovered` in `Compiler/SolidityImport/Coverage.lean`. That predicate is +not `SupportedFunction`: the slice uses `panic`, `returnValues`, packed +`structMember` / `structMember2`, and checked arithmetic, which the IR raccord +does not cover. `DenoteAgreement.execStmt_eq` still holds, including +`Stmt.returnValues`, which records source-order words on `DenoteState` and +finishes as `.stop`. The proof denotation observes success versus revert. It +does not observe the `PanicCode` payload. + +The typed accessors that the import also generates (`example.f`, +`example.position.credit`, ...) add no trust: they are Lean definitions over +the model, through `runFunction` and `readMember` in +`Compiler/SolidityImport/Access.lean`. A theorem stated with them is +kernel-checked against those definitions. They inherit the model's layout, so +they mean the Solidity storage member only as far as the imported layout does. + +The source digest hashes `Import.lean`, `Coverage.lean`, `Report.lean`, +`Quote.lean`, and `Profile.lean` from +the package that contains them: the Verity tree itself, or +`.lake/packages/verity` when a downstream package elaborates the import. The +pinned solc binary stays in the elaborating package's +`.lake/solidity-import/solc-0.8.34`. + +The frontend is in the trust base. A covered model is not a proof that the +model matches the Solidity source or solc's bytecode. `storageLayout` slots and +packed offsets are copied from pinned solc 0.8.34 +(`0.8.34+commit.80d5c536`; linux-amd64 and macosx-amd64 SHA-256 pins in +`Import.lean`). `mappingSlot` in the denotation oracle is not Keccak. Numeric +claims are about the values `structMember` reads back. + +Checked `uint256` and narrower unsigned arithmetic is lowered to `Stmt.ite` plus +`Stmt.panic`, not to a second interpreter. Explicit `uint128(x)` is truncation +(`bitAnd` with `2^128-1`). Ternaries are `Stmt.ite`, so the untaken branch is +not evaluated. Library helpers in an accepted acyclic slice are inlined. +`UtilsLib`-style `min` is the Yul term `xor`/`mul`/`lt`, not a renamed `Expr.min`. +Generated scalar bindings are hygienic, and helper-local bindings are scoped. +Only reached storage fields are decoded. Namespace-qualified output names and +framed-JSON provenance are deterministic. Implicit root returns, named call +arguments, virtual dispatch, and unsupported signed operations are rejected. +The slice makes no dynamic ABI-head claim: that unrelated denotation extension +is not part of this change. Solidity sources are Lake inputs only when the +consumer declares them (`input_dir` + `needs`); consumers should also +re-elaborate in CI to verify freshness against a compiled import. ## Compilation Pipeline @@ -140,7 +65,7 @@ Yul EVM Bytecode ``` -The repository currently has 0 `sorry` placeholders across the `Compiler/**/*.lean` and `Verity/**/*.lean` proof modules that participate in the verified compiler stack. Layer 2 (Source → IR) and Layer 3 (IR → Yul) proof scripts are fully discharged, and it now has 1 documented Lean axiom (`solidityMappingSlot_injective`). See [AXIOMS.md](AXIOMS.md) for details. Audit evidence and generated trust-boundary artifacts are indexed in [AUDIT.md](AUDIT.md). +The repository currently has 0 `sorry` placeholders across the `Compiler/**/*.lean` and `Verity/**/*.lean` proof modules that participate in the verified compiler stack. Layer 2 (Source → IR) and Layer 3 (IR → Yul) proof scripts are fully discharged, and it now has 1 documented Lean axiom (`solidityMappingSlot_injective`). See [AXIOMS.md](AXIOMS.md) for details. Generated trust-boundary artifacts live in `artifacts/`. ## What's Verified @@ -151,7 +76,7 @@ The repository currently has 0 `sorry` placeholders across the `Compiler/**/*.le - **Layer 2**: A generic whole-contract theorem is proved for the current supported `CompilationModel` fragment. `supported_function_correct` is now a real theorem, the initial-state normalization step is proved, the former generic body-simulation axiom has been eliminated, and the theorem surface makes explicit that the observed transaction-context fields must already be normalized to the bounded source-side `Address`/`Uint256` domains. Remaining Layer 2 work is now about widening the supported fragment and helper-aware completeness, not repairing `sorry` placeholders. - **Layer 3**: IR → Yul preservation is generic at the proof surface, and the remaining dispatch bridge now lives as an explicit theorem hypothesis rather than a Lean axiom. The checked contract-level theorem surface makes the dispatch-guard safety preconditions explicit: non-payable cases must see word-level zero `msg.value`, and each selected function case must have a non-wrapping calldata-width guard. -Current theorem totals, property-test coverage, and proof status live in [docs/VERIFICATION_STATUS.md](docs/VERIFICATION_STATUS.md). +Current theorem totals, property-test coverage, and proof status live in [docs/VERIFICATION_STATUS.md](VERIFICATION_STATUS.md). ## Trusted Components @@ -193,7 +118,7 @@ Current theorem totals, property-test coverage, and proof status live in [docs/V - **Role**: Runtime execution model. - **Status (native transition complete)**: native builtin dispatch lives in `Compiler/Proofs/YulGeneration/Backends/EvmYulLeanBuiltinSemantics.lean` and `Compiler/Proofs/YulGeneration/Backends/EvmYulLeanNativeLowering.lean`; `EvmYulLeanBridgeLemmas.lean` records native routing facts for the 36 covered builtin cases, including 25 universal pure bridge theorems. All pure bridge cases are now covered by universal symbolic lemmas. The legacy runtime-oracle stack and builtin comparison oracle have been removed as part of the EVMYulLean transition (DoD-5). The public EndToEnd composition surface in `Compiler/Proofs/EndToEnd.lean` targets native `EvmYul.Yul.callDispatcher` execution through `Compiler.Proofs.YulGeneration.Backends.EvmYulLeanNativeHarness`: the public surface is `nativeResultsMatchOn`, `sourceResultMatchesNativeOn`, the source/native result-composition theorem over that native result surface, and the concrete SimpleStorage native theorem. The fuel-indexed `nativeIRRuntimeMatchesIR` seams are file-local. Gas is not modeled. - **Trust boundary (EVMYulLean EndToEnd target)**: For the native EndToEnd path, the runtime authority is EVMYulLean dispatcher execution after Verity Yul is lowered by the native harness and projected onto the observable result surface. There is no longer a legacy preservation/equivalence stack; only the native chain remains. -- **Fork dependency**: Verity pins [`lfglabs-dev/EVMYulLean`](https://github.com/lfglabs-dev/EVMYulLean), a fork of [`NethermindEth/EVMYulLean`](https://github.com/NethermindEth/EVMYulLean). The pinned commit is recorded in `lake-manifest.json` under the `evmyul` package. The exact divergence from upstream is enumerated in [`artifacts/evmyullean_fork_audit.json`](artifacts/evmyullean_fork_audit.json), regenerated by `scripts/generate_evmyullean_fork_audit.py` and validated by `make check`. As of the current pin, the fork is 18 commits ahead of `upstream/main` and 0 behind: the audited changes are non-semantic visibility, FFI-body-exposure, and Lean 4.22/4.24/4.31 toolchain-compatibility changes; none changes EVM/Yul execution semantics, so upstream Ethereum conformance test coverage continues to apply transitively. In addition to the `make check` validation, a weekly scheduled GitHub Actions workflow ([`.github/workflows/evmyullean-fork-conformance.yml`](.github/workflows/evmyullean-fork-conformance.yml)) runs `make test-evmyullean-fork`, which re-verifies the fork audit artifact against `lake-manifest.json`, checks the EVMYulLean native lowering report, rebuilds the native transition harness, rebuilds the public EndToEnd EVMYulLean target, and rebuilds the native builtin routing lemmas together with the native bridge smoke tests and 0 concrete bridge tests, surfacing any upstream drift as a red workflow plus an automatically opened or updated GitHub issue for scheduled/manual failures. +- **Fork dependency**: Verity pins [`lfglabs-dev/EVMYulLean`](https://github.com/lfglabs-dev/EVMYulLean), a fork of [`NethermindEth/EVMYulLean`](https://github.com/NethermindEth/EVMYulLean). The pinned commit is recorded in `lake-manifest.json` under the `evmyul` package. The exact divergence from upstream is enumerated in [`artifacts/evmyullean_fork_audit.json`](../artifacts/evmyullean_fork_audit.json), regenerated by `scripts/generate_evmyullean_fork_audit.py` and validated by `make check`. As of the current pin, the fork is 18 commits ahead of `upstream/main` and 0 behind: the audited changes are non-semantic visibility, FFI-body-exposure, and Lean 4.22/4.24/4.31 toolchain-compatibility changes; none changes EVM/Yul execution semantics, so upstream Ethereum conformance test coverage continues to apply transitively. In addition to the `make check` validation, a weekly scheduled GitHub Actions workflow ([`.github/workflows/evmyullean-fork-conformance.yml`](../.github/workflows/evmyullean-fork-conformance.yml)) runs `make test-evmyullean-fork`, which re-verifies the fork audit artifact against `lake-manifest.json`, checks the EVMYulLean native lowering report, rebuilds the native transition harness, rebuilds the public EndToEnd EVMYulLean target, and rebuilds the native builtin routing lemmas together with the native bridge smoke tests and 0 concrete bridge tests, surfacing any upstream drift as a red workflow plus an automatically opened or updated GitHub issue for scheduled/manual failures. - **Remaining gap for whole-program retargeting**: The public EndToEnd native surface is in place, but the per-`BridgedStraightStmt` IR↔native observation-equivalence framework that would land truly unconditional S1–S8 / F2/F4/F6/F7 / true S8 has not been built yet — that work is multi-week and tracked separately. The external-call/function-table family now has function-table-aware closure scaffolding in `Compiler/Proofs/YulGeneration/Backends/EvmYulLeanCallClosure.lean` (per-family source-level predicates, per-family `compileStmt`/`compileStmtList` closure theorems, `ECMBridgeable` per-module obligation, and a `BridgedStmts`-preserving `pfx ++ sfx` composition lemma); end-to-end wiring through `SupportedFragment`/`SupportedSpec` for whole contracts using these constructors is the next milestone. - **Implication**: Semantic correctness does not imply gas-safety. - **Proxy note**: `delegatecall`-based proxy / upgradeability flows still sit outside the current native verified runtime model. Archive `--trust-report` and use `--deny-proxy-upgradeability` when proxy semantics must remain outside the selected verified subset (issue `#1420`). @@ -280,7 +205,7 @@ Current theorem totals, property-test coverage, and proof status live in [docs/V ### 7. External Call Modules (ECMs) - **Role**: Reusable typed external call patterns (ERC-20 writes/reads including `totalSupply`, ERC-4626 preview/conversion helpers plus `totalAssets`, `asset`, `max*` limit reads, and `deposit`, oracle reads, precompiles 0x01 / 0x02 / 0x06 / 0x07 / 0x08 — `ecrecover`, `sha256`, BN254 `bn256Add`, `bn256ScalarMul`, `bn256Pairing` — callbacks, and same-contract `selfDelegateMulticallBytes`). - **Trust**: Each module's `compile` produces correct Yul. Bug in one module doesn't affect others. `selfDelegateMulticallBytes` is an explicitly trusted ECM boundary under `self_delegate_multicall_bytes_revert_bubbling`: the compiler emits concrete bounds checks for ABI element offsets, including a pre-add overflow guard before forming the calldata head offset, but full non-empty multicall revert-bubbling semantics are not yet machine-proven. First-class self-delegate sequences (`Verity.MultiContract.denoteSelfDelegateCalls`) are ordinary Lean functions: success threads one world, failure/revert restores the sequence-entry world and records returndata; they do not discharge the ECM assumption. `solidityMappingSlot_injective` remains the one documented Lean axiom. -- **Mitigation**: Axiom aggregation at compile time (`--verbose`), machine-readable trust-surface emission via `--trust-report `, and a fail-closed verification gate via `--deny-unchecked-dependencies` when unchecked foreign surfaces must be excluded. See [docs/EXTERNAL_CALL_MODULES.md](docs/EXTERNAL_CALL_MODULES.md). +- **Mitigation**: Axiom aggregation at compile time (`--verbose`), machine-readable trust-surface emission via `--trust-report `, and a fail-closed verification gate via `--deny-unchecked-dependencies` when unchecked foreign surfaces must be excluded. See [docs/EXTERNAL_CALL_MODULES.md](EXTERNAL_CALL_MODULES.md). - **Summary conformance bridge**: `Verity.Core.Model.SummaryBridge` defines `Conforms`, connecting `StatefulExternal.Summary` (the typed ECM interface contract) to the executable `AdversaryModel` boundary: caller proofs may quantify over summary-conforming adversaries, in which case each summary's `pre`/`post`/`revert` relations and its declared mutability become *proof obligations on the modeled adversary*, not axioms. The staticcall no-external-mutation law is derived from conformance. An adversary's `failure` response carries no summary obligation (opaque exceptional failure; never committed). Whether a given real callee satisfies its summary remains the per-summary trust assumption listed via `assumptionNames`. - **Caller-frame preservation**: The EVM frame condition (external `CALL` cannot mutate caller storage / transient storage / memory outside the declared output buffer) is now a *theorem* of `Verity.EVM.Frame` rather than an assumption. Downstream proofs consume `external_call_preserves_caller_storage` etc. directly. The abstract memory model used by these theorems is `Verity.EVM.MemoryModel`; the solc memory-layout schema and call-buffer-disjoint-from-heap result are in `Verity.EVM.Layout`. EvmYul ↔ abstract-model correspondence remains the open follow-up. @@ -303,7 +228,7 @@ Current theorem totals, property-test coverage, and proof status live in [docs/V ### 9. Macro Elaborator (`verity_contract`) - **Role**: Generates both EDSL `Contract` monad value and `CompilationModel` from one syntax tree. -- **Status**: Trusted unverified metaprogram ([Verity/Macro/Translate.lean](Verity/Macro/Translate.lean)). +- **Status**: Trusted unverified metaprogram ([Verity/Macro/Translate.lean](../Verity/Macro/Translate.lean)). - **Risk**: A translation bug would silently cause EDSL and CompilationModel to diverge. - **Mitigation**: The generic Layer 2 whole-contract theorem, macro-generated `_semantic_preservation` body-alignment checks, and differential tests catch divergence on the current contract set. @@ -326,7 +251,7 @@ Current theorem totals, property-test coverage, and proof status live in [docs/V `calldatacopy` first-class only when Verity has stable semantics and they are useful for proofs. Treat other raw Yul as an explicit trust-report surface, and use `--deny-local-obligations` / low-level deny gates for strict builds. -- **Reference**: See [docs/LOW_LEVEL_YUL.md](docs/LOW_LEVEL_YUL.md). +- **Reference**: See [docs/LOW_LEVEL_YUL.md](LOW_LEVEL_YUL.md). ### 11. Consumer-Declared Intrinsics - **Role**: Let downstream packages bind a source-level Verity function to a @@ -353,7 +278,7 @@ Current theorem totals, property-test coverage, and proof status live in [docs/V status. Fork enforcement is fail-closed: a contract targeting a fork lower than an intrinsic's `min_fork` errors unless `--allow-future-fork-intrinsics` is passed. -- **Reference**: See [docs/INTRINSICS.md](docs/INTRINSICS.md). +- **Reference**: See [docs/INTRINSICS.md](INTRINSICS.md). ## Semantic Caveats @@ -405,10 +330,10 @@ module output forces opacity. The ECM wrapper lemma preserves bridge syntactic support, not execution semantics; these checks introduce no new proof claim. This is an existing compiler-validation boundary whose behavior changed with the structured-panic migration; it introduces no new Lean axiom. See -[docs/ARITHMETIC_PROFILE.md](docs/ARITHMETIC_PROFILE.md). +[docs/ARITHMETIC_PROFILE.md](ARITHMETIC_PROFILE.md). ### Revert-State Modeling -High-level semantics can expose intermediate state in reverted computations. EVM reverts discard state. Contracts should use checks-before-effects. See [docs/REVERT_STATE_MODEL.md](docs/REVERT_STATE_MODEL.md). +High-level semantics can expose intermediate state in reverted computations. EVM reverts discard state. Contracts should use checks-before-effects. See [REVERT_STATE_MODEL.md](REVERT_STATE_MODEL.md). ### Top-Level Transaction Rollback (`denoteTransaction`) `Verity.Core.Model.CallProgramRollback.denoteTransaction` restores the pre-transaction caller world on a top-level `.revert` *by construction*: the `denoteTransaction_revert_*` theorems characterize this wrapper's behavior, they do not derive rollback from lower-level EVM semantics. That the EVM's actual transaction-revert behavior matches this model (full world restoration, gas remains charged, returndata exposed) is a trusted modeling assumption, on par with §6 (EVM/Yul Semantics). @@ -724,8 +649,8 @@ global invariant `I : ContractState → Prop`. invariant `I`. The lock-as-disjunct idiom (`I s := healthy s ∨ locked s`) is the recommended pattern for trade-window safety, but `I` itself is author-chosen and not verified against any external specification. -- **Reference**: [Verity/Core/Reentrancy.lean](Verity/Core/Reentrancy.lean), - [Contracts/ReentrancyRelyGuarantee/Contract.lean](Contracts/ReentrancyRelyGuarantee/Contract.lean). +- **Reference**: [Verity/Core/Reentrancy.lean](../Verity/Core/Reentrancy.lean), + [Contracts/ReentrancyRelyGuarantee/Contract.lean](../Contracts/ReentrancyRelyGuarantee/Contract.lean). ### Cross-Function Reentrancy Gate (fail-closed) Every function whose body opens a reentrancy window (`externalCallBind`, @@ -734,7 +659,7 @@ Every function whose body opens a reentrancy window (`externalCallBind`, mechanic) and that is **not** `view`/`pure` must declare a reentrancy disposition or compilation fails (`validateReentrancyDisposition`, keyed on `stmtOpensReentrancyWindow`, in -[Compiler/CompilationModel/Validation.lean](Compiler/CompilationModel/Validation.lean)). +[Compiler/CompilationModel/Validation.lean](../Compiler/CompilationModel/Validation.lean)). This is a dedicated pass that runs *after* call well-formedness validation, so a malformed external call surfaces its structural error first and the reentrancy policy only judges otherwise well-formed calls. It is also independent of the @@ -785,7 +710,7 @@ double-guarding the already-guarded public chain). Consequently a *lock-only* `nonreentrant()` function (one that does **not** also carry `reentrancy_trusted`) **cannot be invoked as an internal helper**: such a call is rejected at macro lowering (`ensureCallableAsInternalHelper`, -[Verity/Macro/Internal.lean](Verity/Macro/Internal.lean)), because routing it +[Verity/Macro/Internal.lean](../Verity/Macro/Internal.lean)), because routing it through the lock-free shadow would run the guarded body without the lock and silently bypass the only reentrancy protection it had. Calling a function that *also* carries `reentrancy_trusted` is accepted — that disposition is a global @@ -793,14 +718,14 @@ author assertion covering every entry path, including the lock-free internal one This mirrors the parse-time rejection of an `internal nonreentrant()` helper (`NonreentrantInternalHelperRejected`, #1971). -- **Reference**: [Compiler/CompilationModel/Validation.lean](Compiler/CompilationModel/Validation.lean) +- **Reference**: [Compiler/CompilationModel/Validation.lean](../Compiler/CompilationModel/Validation.lean) (`validateReentrancyDisposition`, the cross-function reentrancy gate — a dedicated pass run after call well-formedness, distinct from the single-function CEI check in `validateFunctionSpec`); - [Verity/Macro/Internal.lean](Verity/Macro/Internal.lean) + [Verity/Macro/Internal.lean](../Verity/Macro/Internal.lean) (`ensureCallableAsInternalHelper`, the lowering-time rejection of internal calls to lock-only `nonreentrant` entries). -- **Regression evidence**: [Contracts/Smoke/SecurityCombos.lean](Contracts/Smoke/SecurityCombos.lean) +- **Regression evidence**: [Contracts/Smoke/SecurityCombos.lean](../Contracts/Smoke/SecurityCombos.lean) pairs `ReentrancyDispositionRequired` (a CEI-clean `take` rejected by the gate, asserted via `#guard_msgs`) with `ReentrancyDispositionDeclared` (the identical body accepted once it carries `reentrancy_trusted`) — the Midnight @@ -826,8 +751,8 @@ helper (`NonreentrantInternalHelperRejected`, #1971). ## Related Documents -- [AUDIT.md](AUDIT.md) | [AXIOMS.md](AXIOMS.md) | [docs/INTRINSICS.md](docs/INTRINSICS.md) | [docs/ARITHMETIC_PROFILE.md](docs/ARITHMETIC_PROFILE.md) | [docs/REVERT_STATE_MODEL.md](docs/REVERT_STATE_MODEL.md) -- [docs/EXTERNAL_CALL_MODULES.md](docs/EXTERNAL_CALL_MODULES.md) | [docs/ROADMAP.md](docs/ROADMAP.md) | [docs/VERIFICATION_STATUS.md](docs/VERIFICATION_STATUS.md) +- [AXIOMS.md](AXIOMS.md) | [INTRINSICS.md](INTRINSICS.md) | [ARITHMETIC_PROFILE.md](ARITHMETIC_PROFILE.md) | [REVERT_STATE_MODEL.md](REVERT_STATE_MODEL.md) +- [EXTERNAL_CALL_MODULES.md](EXTERNAL_CALL_MODULES.md) | [ROADMAP.md](ROADMAP.md) | [VERIFICATION_STATUS.md](VERIFICATION_STATUS.md) --- diff --git a/docs/UNLINK_AUDIT_VERITY_CORE_PROMPT.md b/docs/UNLINK_AUDIT_VERITY_CORE_PROMPT.md deleted file mode 100644 index 0781ff90c1..0000000000 --- a/docs/UNLINK_AUDIT_VERITY_CORE_PROMPT.md +++ /dev/null @@ -1,239 +0,0 @@ -# Prompt: Unlink Audit Readiness In Verity Core - -You are working in the `lfglabs-dev/verity` repository. The priority is to make -the Verity-core language/compiler surface clean enough for a line-by-line Unlink -audit model, while keeping Unlink-specific cryptographic and protocol -assumptions out of Verity core. - -Do not implement an Unlink model in this repository. Do not add built-in -Poseidon, Permit2, Lazy-IMT, or Groth16 verifier semantics to Verity core. Those -belong in a separate `unlink-verity` package as external declarations or -package-local ECMs with explicit axiom names and proof-status tags. - -## Goal - -Implement and document the generic Verity-core features needed to express -Unlink cleanly: - -1. ETH-aware generic external call ECM. -2. ECM/trust-boundary documentation sync. -3. Verification/docs for mutable accumulators in `forEach`. -4. Verification/docs for multi-value internal helper returns. -5. `abiEncodePacked` helper. -6. `sha256` / `sha256Packed` helper. -7. BN254 scalar field constant and `modField` helper. - -## Current Status Checkpoint - -Before starting new work, inspect the current repository state and avoid -reimplementing items that are already present. As of this checkpoint, the -expected Verity-core surface is: - -- `Compiler.Modules.Calls.bubblingValueCall` for generic - `call{value: v}(data)` over explicit memory slices, with revert-returndata - bubbling and trust-report metadata. Use - `Compiler.Modules.Calls.bubblingValueCallNoOutput` for the common adapter - path that ignores successful returndata, or - `Compiler.Modules.Calls.bubblingValueCallNoOutputModule` from - `verity_contract` `ecmDo` call sites. -- `Compiler.Modules.Precompiles.sha256Memory` and the shorter - `Compiler.Modules.Precompiles.sha256` alias for SHA-256 precompile 0x02 over - caller-provided memory slices. -- `Compiler.Modules.Hashing.abiEncodePackedWords` / - `Compiler.Modules.Hashing.abiEncodePacked` and - `Compiler.Modules.Hashing.sha256PackedWords` / - `Compiler.Modules.Hashing.sha256Packed` for static 32-byte-word packed - preimages. -- `Compiler.Modules.Hashing.abiEncodePackedStaticSegments` and - `Compiler.Modules.Hashing.sha256PackedStaticSegments` for explicit 1- to - 32-byte static segments such as address-sized preimages. -- `Verity.Stdlib.Math.SNARK_SCALAR_FIELD` and - `Verity.Stdlib.Math.modField`. -- Compilation-model tests for mutable `Stmt.assignVar` inside `Stmt.forEach` - and macro smoke coverage for tuple-return helper bindings lowering to - `Stmt.internalCallAssign`. - -Known remaining limitation: `verity_contract` executable fallback source still -rejects mutating a Lean `let mut` local from inside a nested `forEach` body. -The compilation model supports the desired accumulator shape directly via -`Stmt.assignVar`; macro-authored contracts that need executable fallback tests -should use explicit scratch storage/memory until the fallback limitation is -removed. - -Good follow-up work should either remove that fallback limitation cleanly, add -tests/docs around a genuinely missing edge case, or tighten trust-boundary -documentation. Do not keep adding parallel helper names for already-covered -static packed hashing semantics. - -## Required Implementation Order - -### 1. ETH-aware generic external call ECM - -Add a reusable module under `Compiler/Modules/Calls.lean` for Solidity-style -arbitrary external calls equivalent to: - -```solidity -(bool ok, bytes memory returndata) = target.call{value: value}(data); -if (!ok) { - assembly { - revert(add(returndata, 32), mload(returndata)) - } -} -``` - -The API should be protocol-agnostic and usable by UnlinkAdapter-style execution -of arbitrary call data. It should support: - -- target address -- ETH value -- calldata/input offset and size, or the closest existing bytes-like Verity - surface if one already exists -- configurable output handling if the existing ECM design supports it -- exact revert-data bubbling on failed calls -- correct `writesState`, `readsState`, `axioms`, and `proofStatus` metadata -- clear validation errors for unsupported argument shapes - -Do not weaken CEI or mutability checks. If the module writes state, it must be -marked accordingly. - -Add compile-time tests in `Compiler/CompilationModelFeatureTest.lean` covering: - -- lowering uses `call`, not `staticcall` -- the call value operand is wired from the user argument -- failure path copies `returndatasize()` bytes and reverts with that payload -- invalid argument counts fail closed -- trust-report / ECM metadata remains visible through existing reporting paths - if there is an existing test pattern for that - -### 2. Documentation sync - -Update: - -- `docs/EXTERNAL_CALL_MODULES.md` -- `docs/INTERPRETER_FEATURE_MATRIX.md` -- `docs/ROADMAP.md` - -The docs must state the package split: - -- Verity core owns generic Solidity/EVM modeling surfaces such as low-level call - mechanics, returndata bubbling, ABI helper ergonomics, loop/mutable-local - syntax, multi-value helper return syntax, and trust reporting. -- `unlink-verity` owns Unlink-specific assumptions for Poseidon, Permit2, - Lazy-IMT, and Groth16 verifier behavior. - -Avoid wording that implies the full Unlink model or Unlink-specific assumptions -should live in Verity core. - -### 3. Mutable accumulators inside `forEach` - -Check whether macro syntax already supports this pattern: - -```lean -let mut total := 0 -forEach "i" (arrayLength notes) (do - let amount := (arrayElement notes i).amount - total := add total amount) -return total -``` - -If it already works, add a focused smoke/feature test and docs example. If it -does not work, first preserve the current boundary with a regression test and -docs. Then implement the smallest macro/EDSL change needed so assignment to a -mutable local inside `forEach` lowers to `Stmt.assignVar`, including whatever -executable fallback adjustment is needed for `verity_contract` source terms. - -Do not introduce a separate `forEachAccum` unless the existing assignment syntax -cannot be made to work cleanly. - -### 4. Multi-value internal helper returns - -Check whether macro syntax already supports `_buildPublicSignals`-style helper -returns. Prefer a syntax that maps to existing `Stmt.internalCallAssign`, for -example a tuple binding if that is already supported by local conventions. - -Add a smoke/feature test and docs example. If macro support is missing, add the -smallest syntax extension that reuses the existing compilation-model -`Stmt.internalCallAssign` machinery. - -### 5. `abiEncodePacked` helper - -Add helpers for static packed encoding used in hash preimages. The first version -may support only a list of word-sized `Expr` values; a higher-ROI follow-up is a -static segment helper with explicit 1- to 32-byte widths for address-sized and -other sub-word preimages. - -Expected behavior: - -- writes each word at consecutive 32-byte offsets -- for segment helpers, left-aligns sub-word values and places subsequent - segments at byte-precise offsets -- computes the correct total byte length -- returns `keccak256(offset, length)` or expands to existing `Expr.keccak256` - support -- avoids ad hoc offset arithmetic in user contracts - -Add tests that inspect generated Yul or compilation-model output for correct -offsets and length. - -### 6. `sha256` / `sha256Packed` helper - -Add a helper for SHA-256 precompile 0x02: - -- emits `staticcall(gas(), 2, inputOffset, inputSize, outputOffset, 32)` -- reverts on precompile failure -- returns `mload(outputOffset)` as the digest word - -If `sha256Packed` is added, it should reuse the same packed memory layout as -`abiEncodePacked` but route to the SHA-256 precompile instead of `keccak256`. - -Add generated-Yul tests for precompile address, input size, output size, and -failure handling. - -### 7. BN254 scalar helper - -Add a generic, documented helper for circuit-facing field reductions: - -```lean -SNARK_SCALAR_FIELD = -21888242871839275222246405745257275088548364400416034343698204186575808495617 - -modField x = mod x SNARK_SCALAR_FIELD -``` - -Place it in a generic arithmetic/ZK helper location consistent with the repo's -existing module layout. Add a small test or compile-time check. - -## Already Done: Do Not Reimplement - -- `address(this).balance` / `selfbalance()` support has already landed. -- Named struct field access on calldata array elements is already supported via - `struct` declarations and `(arrayElement xs i).field`. -- ERC-20 `balanceOf`, `allowance`, and `totalSupply` ECMs already exist in - `Compiler/Modules/ERC20.lean`. -- Low-level primitives such as `mstore`, `keccak256`, `staticcall`, `call`, - `returndataSize`, `returndataCopy`, and `revertReturndata` already exist at - the macro or compilation-model level. - -## Verification - -Before opening a PR, run as much local validation as practical: - -```bash -lake build Contracts Compiler PrintAxioms -python3 scripts/generate_print_axioms.py --check -make check -``` - -If a narrower test is added for a specific feature, run it directly as well. -Report any tests that could not be run and why. - -## PR Expectations - -Keep the PR Verity-core only. The final PR description should include: - -- summary of implemented generic features -- tests run -- exact docs changed -- explicit statement that Poseidon, Permit2, Lazy-IMT, Groth16, and the full - Unlink model remain in `unlink-verity` -- trust-boundary notes for every new ECM or helper that emits low-level calls diff --git a/docs/VERIFICATION_STATUS.md b/docs/VERIFICATION_STATUS.md index 7087556a80..251e06814a 100644 --- a/docs/VERIFICATION_STATUS.md +++ b/docs/VERIFICATION_STATUS.md @@ -39,14 +39,12 @@ EVM Bytecode | ERC20 | 22 | Baseline | `Contracts/ERC20/Proofs/` | | ERC721 | 11 | Baseline | `Contracts/ERC721/Proofs/` | | Vault | 9 | Baseline | `Contracts/Vault/Proofs/` | -| VaultFromSolidity | 7 | Proof-only import | `Contracts/VaultFromSolidity/Proofs/` | -| SolidityImportSmoke | 20 | Proof-only import | `Contracts/SolidityImportSmoke/` | | ReentrancyExample | 5 | Complete | `Contracts/ReentrancyExample/Contract.lean` | | ReentrancyRelyGuarantee | 10 | Semantic | `Contracts/ReentrancyRelyGuarantee/Contract.lean` | | CryptoHash | 0 | No specs | `Contracts/CryptoHash/Contract.lean` | -| **Total** | **356** | **✅ 100%** | — | +| **Total** | **329** | **✅ 100%** | — | -> **Note**: Stdlib (0 internal proof-automation properties) is excluded from the contract-spec theorem table above but included in overall coverage statistics (356 total properties). +> **Note**: Stdlib (0 internal proof-automation properties) is excluded from the contract-spec theorem table above but included in overall coverage statistics (329 total properties). Layer 1 uses macro-generated EDSL-to-`CompilationModel` bridge theorems backed by a generic typed-IR compilation-correctness theorem ([`TypedIRCompilerCorrectness.lean`](../Compiler/TypedIRCompilerCorrectness.lean)). Tuple/bytes/fixed-array/dynamic-array/string parameters now stay inside that proof path when they are carried as ABI head words/offsets. Advanced constructs beyond that typed-IR head-word surface (linked libraries, ECMs, fully custom ABI behavior) are still expressed directly in `CompilationModel` and trusted at that boundary. Higher-order internal helpers (function-pointer parameters, [#1747](https://github.com/lfglabs-dev/verity/issues/1747)) are eliminated by a compile-time monomorphization pre-pass that runs before any lowering, so the `CompilationModel` only ever contains first-order helpers: these calls are covered by the existing first-order proof path and introduce no new boundary trust. @@ -118,7 +116,7 @@ IR/source semantics on the observable storage/log/result surface. The old fuel-parametric Verity-side Yul executor and preservation/equivalence files are no longer part of the checked-in proof tree. -Key files: [`EndToEnd.lean`](../Compiler/Proofs/EndToEnd.lean), [`EvmYulLeanNativeHarness.lean`](../Compiler/Proofs/YulGeneration/Backends/EvmYulLeanNativeHarness.lean), [`EvmYulLeanBodyClosure.lean`](../Compiler/Proofs/YulGeneration/Backends/EvmYulLeanBodyClosure.lean), [`EvmYulLeanBridgeLemmas.lean`](../Compiler/Proofs/YulGeneration/Backends/EvmYulLeanBridgeLemmas.lean), [`AXIOMS.md`](../AXIOMS.md) +Key files: [`EndToEnd.lean`](../Compiler/Proofs/EndToEnd.lean), [`EvmYulLeanNativeHarness.lean`](../Compiler/Proofs/YulGeneration/Backends/EvmYulLeanNativeHarness.lean), [`EvmYulLeanBodyClosure.lean`](../Compiler/Proofs/YulGeneration/Backends/EvmYulLeanBodyClosure.lean), [`EvmYulLeanBridgeLemmas.lean`](../Compiler/Proofs/YulGeneration/Backends/EvmYulLeanBridgeLemmas.lean), [`AXIOMS.md`](AXIOMS.md) ### Phase 4: EVMYulLean Native Dispatcher (safe-body EndToEnd target) @@ -204,8 +202,6 @@ Also note that the macro-generated `*_semantic_preservation` theorems are not co |----------|----------|------------| | ERC20 | 86% (19/22) | 3 proof-only | | Vault | 0% (0/9) | 9 proof-only | -| VaultFromSolidity | 0% (0/7) | 7 proof-only | -| SolidityImportSmoke | 0% (0/20) | 20 proof-only | | ERC721 | 100% (11/11) | 0 | | SafeCounter | 100% (25/25) | 0 | | ReentrancyExample | 100% (5/5) | 0 | @@ -221,11 +217,11 @@ Also note that the macro-generated `*_semantic_preservation` theorems are not co | Counter | 74% (23/31) | 8 proof-only | | Stdlib | 0% (0/0) | 0 proof-only | -**Status**: 72% coverage (255/356), 101 remaining exclusions all proof-only +**Status**: 78% coverage (255/329), 74 remaining exclusions all proof-only -- **Total Properties**: 356 +- **Total Properties**: 329 - **Covered**: 255 -- **Excluded**: 101 (all proof-only) +- **Excluded**: 74 (all proof-only) **Proof-Only Properties (74 exclusions)**: Internal proof machinery that cannot be tested in Foundry. @@ -279,7 +275,7 @@ Diagnostics policy for unsupported constructs: ## Trust Assumptions -See [`TRUST_ASSUMPTIONS.md`](../TRUST_ASSUMPTIONS.md) for the full trust model and [`AXIOMS.md`](../AXIOMS.md) for axiom documentation. +See [`TRUST_ASSUMPTIONS.md`](TRUST_ASSUMPTIONS.md) for the full trust model and [`AXIOMS.md`](AXIOMS.md) for axiom documentation. --- diff --git a/docs/archive/IR_STORAGE_PHASE1_PLAN.md b/docs/archive/IR_STORAGE_PHASE1_PLAN.md deleted file mode 100644 index e43c8033d6..0000000000 --- a/docs/archive/IR_STORAGE_PHASE1_PLAN.md +++ /dev/null @@ -1,79 +0,0 @@ -# IR Storage Refactor — Phase 1 Plan - -Phase 1 of [`IR_STORAGE_UINT256_REFACTOR.md`](../IR_STORAGE_UINT256_REFACTOR.md): flip -`IRStorageWord` from its Phase-0 `abbrev := Nat` surface to a `UInt256`-backed -representation, and audit every former `Nat → Nat` callsite that flowed through -`IRState.storage`. - -This file is the working scaffold for the Phase 1 PR. It is intentionally a -plan-only document so the PR opens against a green build; the actual carrier -flip lands in subsequent commits on this branch. - -## Entry state - -- `Compiler/Proofs/IRGeneration/IRStorageWord.lean` defines - `abbrev IRStorageWord := Nat` plus `ofNat` / `toNat` / `toUInt256` helpers - and round-trip lemmas (PR #1753, Phase 0). -- `IRState.storage : Nat → IRStorageWord` is rfl-identical to `Nat → Nat` - under the abbrev, so the Phase 0 retype required no callsite changes. -- The public theorem `simpleStorage_endToEnd_native_evmYulLean` originally - retained `hRetrieveHit` and `hStoreHit` as explicit hypotheses (#1743 commit - `fe63b826`); later native bridge work discharged those premises. - -## Phase 1 deliverables - -1. Replace `abbrev IRStorageWord := Nat` with a structurally-bounded - representation — preferred form: `def IRStorageWord := UInt256` (or - a single-field structure wrapping `UInt256` if `def` opacity proves - awkward in proof contexts). -2. Update `IRStorageWord.ofNat` / `toNat` so that `ofNat n = ofNat (n % UInt256.size)` - holds definitionally on the new carrier; expose the masking lemma. -3. Update the IR interpreter `sload` / `sstore` semantics in - `Compiler/Proofs/IRGeneration/IRInterpreter.lean` to read and write through - the typed helpers rather than treating the carrier as raw `Nat`. -4. Audit log (table below) — every callsite that used to touch - `IRState.storage : Nat → Nat` must be inspected, marked `safe` (no value - semantics affected) or `migrate` (needs an explicit `ofNat` / `toNat` call), - and its outcome recorded. -5. Confirm `lake build` clean and every existing per-contract spec proof in - `Contracts//Proofs/` still passes (no spec regressions). - -## Audit log - -| Callsite | File:Line | Treatment | Notes | -|----------|-----------|-----------|-------| -| `IRStorageWord` carrier | `Compiler/Proofs/IRGeneration/IRStorageWord.lean` | migrate | Flipped from `abbrev := Nat` to `@[reducible] def := UInt256`. Added `OfNat`, `Inhabited` instances and reproved round-trip lemmas (`toNat_ofNat = n % UInt256.size`, `ofNat_toNat = w`, `toNat_lt_size`). | -| `abstractStoreMappingEntry` | `Compiler/Proofs/MappingSlot.lean:83` | migrate | Wraps `value : Nat` via `IRStorageWord.ofNat` inside the helper; simp lemma `abstractStoreMappingEntry_eq` updated to match. | -| `abstractStoreStorageOrMapping` | `Compiler/Proofs/MappingSlot.lean:95` | migrate | Wraps `value : Nat` via `IRStorageWord.ofNat`; simp lemma `abstractStoreStorageOrMapping_eq` updated. | -| `evalBuiltinCallViaEvmYulLean` (sload) | `Compiler/Proofs/YulGeneration/Backends/EvmYulLeanNativeLowering.lean` | migrate | Same `.toNat` projection on the native EVMYulLean lowering sload branch. | -| `IRInterpreter.evalIRStatementWithBackend` | `Compiler/Proofs/IRGeneration/IRInterpreter.lean` | safe | All `state.storage` reads go through `abstractLoadStorageOrMapping`, all writes go through `abstractStoreStorageOrMapping` / `abstractStoreMappingEntry`, so the helper-internal wrapping covers every callsite without changes here. Initial-state literal `fun _ => 0` continues to elaborate via the `OfNat IRStorageWord 0` instance. | -| `EvmYulLeanBridgeLemmas` | `Compiler/Proofs/YulGeneration/Backends/EvmYulLeanBridgeLemmas.lean` | safe | Builtin-equivalence lemmas thread `storage : Nat → IRStorageWord` as a parameter but never inspect the value; survive carrier flip without edits. | -| Per-contract specs `Contracts/*/Proofs/` | (across the tree) | safe | Spec-side proofs reason over `IRState`/`abstractLoad…` API surface; no callsite touches the raw carrier. `lake build` + `make check` (600 tests) clean post-flip. | - -## Risks tracked from the parent doc - -- Spec drift on `decide`-style spec proofs that pattern-matched on raw `Nat` - storage values. Mitigation: keep helper API stable, expose `Nat`-shaped - re-exports at the spec boundary if needed. -- `UInt256` arithmetic is heavier than `Nat` for kernel reduction. If a - benchmark regresses, expose dedicated `simp` lemmas before relaxing the - carrier. - -## Exit criteria - -- `IRStorageWord` is no longer an `abbrev` for `Nat`. -- `lake build` clean. -- `make check` clean. -- Every `Contracts/*/Proofs/` spec theorem unchanged (no signature drift). -- Public theorem `simpleStorage_endToEnd_native_evmYulLean` keeps building; - the later Phase 2 / Phase 3 work discharges the retrieve/store hit premises. - -## Status - -Carrier flip implemented. `IRStorageWord` is now `@[reducible] def := UInt256` -with `OfNat` / `Inhabited` instances and the round-trip lemmas restated for the -new carrier. Downstream callsites have been migrated (helpers wrap `Nat → IRStorageWord` -internally; sload sites project via `.toNat`). `lake build` clean, `make check` -green (600 tests). The later Phase 2 / Phase 3 bridge work discharges the -`hRetrieveHit` / `hStoreHit` premises from -`simpleStorage_endToEnd_native_evmYulLean`. diff --git a/docs/archive/IR_STORAGE_PHASE2_PLAN.md b/docs/archive/IR_STORAGE_PHASE2_PLAN.md deleted file mode 100644 index ca7a66c58e..0000000000 --- a/docs/archive/IR_STORAGE_PHASE2_PLAN.md +++ /dev/null @@ -1,84 +0,0 @@ -# IR Storage Refactor — Phase 2 Plan - -Phase 2 of [`IR_STORAGE_UINT256_REFACTOR.md`](../IR_STORAGE_UINT256_REFACTOR.md) -originally tracked discharging the retrieve-hit fuel-wrapper bridge after the -`UInt256`-bounded IR storage carrier landed. - -Status update: this plan has been superseded for the public native path. The -public `simpleStorage_endToEnd_native_evmYulLean` theorem now consumes the -direct native-vs-IR `simpleStorageNativeRetrieveHitMatchBridge_proved` proof -through `simpleStorageNativeCallDispatcherMatchBridge_of_per_case`. The older -retrieve-hit compatibility theorem has been -removed with the obsolete SimpleStorage fuel-wrapper bridge surface. - -This file is the working scaffold for the Phase 2 PR. It is plan-only so the -PR opens against a green build. - -## Prerequisite - -Phase 1 (#1754) must land first. Phase 2 is a no-op without the bounded carrier -because the storage-modulo gap is what blocks the bridge. - -## Why this becomes provable after Phase 1 - -After Phase 1, every value flowing through `IRState.storage` is `UInt256`-bounded -by construction. That eliminates the two structural blockers documented in the -parent transition note: - -1. **Storage-value gap on the projected `finalStorage`.** Native projection - applies `% UInt256.size`; the IR oracle reads raw `Nat`. Once the IR carrier - is `UInt256`, the modulo becomes the identity on every observable slot — - the two sides agree definitionally up to the `IRStorageWord.toNat ∘ ofNat` - round-trip lemma exposed by Phase 0. -2. **Return-value gap from `mstore` byte buffer.** The retrieve-hit return is - built by `mstore` of a `UInt256` and re-extracted by `projectHaltReturn` from - `state.sharedState.H_return`. With the IR oracle's pre-`mstore` value already - `UInt256`-bounded, the `mstore` step is no longer lossy: the same 32 bytes - land in the buffer on both sides. - -## Phase 2 deliverables - -1. Lemma `simpleStorageNativeRetrieveHitMatchBridge_proved` — analogous to the - direct selector-miss native match proof. -2. Replace the explicit `hRetrieveHit` premise on - `simpleStorage_endToEnd_native_evmYulLean` with the proved lemma. -3. `PrintAxioms` for the public theorem no longer lists the retrieve-hit - bridge. -4. `Contracts/SimpleStorage/Proofs/` unchanged. - -## Proof obligation outline - -The current bridge statement (see `Compiler/Proofs/EndToEnd.lean`) reduces, after -the carrier flip, to: -- The native dispatcher's hit-case body executes the `sload` against - `SharedState.storage`, returns its `UInt256` payload, `mstore`s it to memory - at the canonical return offset, and halts with `H_return` covering those 32 - bytes. -- The IR oracle's hit-case path computes the same `UInt256` value via - `IRStorageWord.toUInt256 ∘ IRState.storage`. -- After Phase 1, both sides reduce to the same `UInt256` and therefore to the - same projected `Nat`. The bridge closes by `rfl` modulo the round-trip - lemmas already exported from `IRStorageWord.lean`. - -## Risks - -- The native side's `mstore`/`H_return` chain still requires byte-level - alignment lemmas at the `projectHaltReturn` boundary. Those lemmas exist - for the selector-miss case; the retrieve-hit reuse must match offset and - length conventions exactly. -- If Phase 1 leaves any spec theorem reading raw `Nat` from storage at the - spec boundary, the retrieve-hit lemma will need a `toNat`-shape adapter. - -## Exit criteria - -- `simpleStorageNativeRetrieveHitMatchBridge_proved` lands and is invoked at the - call site of the former `hRetrieveHit` premise. -- `simpleStorage_endToEnd_native_evmYulLean` no longer carries `hRetrieveHit`. -- `PrintAxioms` reflects the drop. -- `lake build` and `make check` clean. - -## Status - -Superseded for the public native SimpleStorage theorem. The direct retrieve-hit -proof is `simpleStorageNativeRetrieveHitMatchBridge_proved`; remaining generic -compatibility work is tracked in the active native EndToEnd theorem surface. diff --git a/docs/archive/IR_STORAGE_PHASE3_PLAN.md b/docs/archive/IR_STORAGE_PHASE3_PLAN.md deleted file mode 100644 index a8d0fd9b20..0000000000 --- a/docs/archive/IR_STORAGE_PHASE3_PLAN.md +++ /dev/null @@ -1,91 +0,0 @@ -# IR Storage Refactor — Phase 3 Plan - -Phase 3 of [`IR_STORAGE_UINT256_REFACTOR.md`](../IR_STORAGE_UINT256_REFACTOR.md) -originally tracked discharging the store-hit fuel-wrapper bridge and dropping the -`hStoreHit` premise from `simpleStorage_endToEnd_native_evmYulLean`. - -Status update: this plan has been superseded for the public native path. The -public `simpleStorage_endToEnd_native_evmYulLean` theorem now consumes the -direct native-vs-IR `simpleStorageNativeStoreHitMatchBridge_proved` proof -through `simpleStorageNativeCallDispatcherMatchBridge_of_per_case`. The older -store-hit compatibility theorem has been -removed with the obsolete SimpleStorage fuel-wrapper bridge surface. - -This file is the working scaffold for the Phase 3 PR. It is plan-only so the -PR opens against a green build. - -## Prerequisites - -- Phase 1 (#1754) — `UInt256`-bounded IR storage carrier. -- Phase 2 (#1755) — retrieve-hit bridge proved and `hRetrieveHit` removed. - -## Why this becomes provable after Phase 1 - -The store-hit case has two distinct read paths and one write path: - -1. **The sstore-written slot.** Calldata round-trip is unconditionally bounded - to 32 bytes by the lowered dispatcher, so the WRITTEN value was already - `UInt256`-bounded; no Phase 1 dependency for that arm. -2. **Re-read of any other materialized observable slot.** This is the same - gap as Phase 2 — Phase 1's carrier flip closes it. -3. **Storage-state agreement after the write.** The native side mutates - `SharedState.storage` at slot index `s`; the IR oracle mutates - `IRState.storage` at the same index. Once both carriers are `UInt256`, - the post-state functional extensionality argument over slots reduces - identically on both sides. - -## Phase 3 deliverables - -1. Lemma `simpleStorageNativeStoreHitMatchBridge_proved` — mirrors the Phase 2 - retrieve-hit lemma, with the additional sstore mutation argument. -2. Replace the explicit `hStoreHit` premise on - `simpleStorage_endToEnd_native_evmYulLean` with the proved lemma. -3. `PrintAxioms` for the public theorem reports no remaining bridge premises - beyond the trusted EVMYulLean builtin axioms. -4. `simpleStorageNativeCallDispatcherMatchBridge_of_per_case` is the surviving - dispatcher surface and is fully closed. -5. `Contracts/SimpleStorage/Proofs/` unchanged. - -## Proof obligation outline - -After Phases 1 and 2, the store-hit bridge reduces to: - -- The native dispatcher's hit-case body executes `sstore(slot, calldata-word)` - on `SharedState.storage`, then halts. `H_return` is empty for the store - case. -- The IR oracle's hit-case path applies the same functional update to - `IRState.storage`. -- After Phase 1, both carriers are `UInt256`; the functional update agrees on - every slot index by `Function.update`-style reasoning. -- The empty `H_return` matches trivially. - -## Risks - -- The `Function.update`-style equality proof requires decidable equality on - the slot index. `Nat` provides it; the bounded carrier does not change the - index type, only the value type — so this should remain mechanical. -- Any spec theorem that explicitly observes `IRState.storage` after a write - may need a `Function.update_eq` rewrite at the spec boundary. Phase 1's - audit log should already enumerate these sites. - -## Exit criteria - -- `simpleStorageNativeStoreHitMatchBridge_proved` lands and is invoked at the - call site of the former `hStoreHit` premise. -- `simpleStorage_endToEnd_native_evmYulLean` has zero remaining bridge - premises. -- `PrintAxioms` reflects the drop. -- `lake build` and `make check` clean. - -## After Phase 3 - -Phase 4 — generalize the per-case bridge surface to a dispatcher-shape-driven -generic so a second contract (e.g. Counter) lifts to the native theorem -without per-contract bridge code. That is intentionally out of scope for this -PR. - -## Status - -Superseded for the public native SimpleStorage theorem. The direct store-hit -proof is `simpleStorageNativeStoreHitMatchBridge_proved`; remaining generic -compatibility work is tracked in the active native EndToEnd theorem surface. diff --git a/docs/archive/NATIVE_EVMYULLEAN_DONE_GRAPH.md b/docs/archive/NATIVE_EVMYULLEAN_DONE_GRAPH.md deleted file mode 100644 index dadb096cfd..0000000000 --- a/docs/archive/NATIVE_EVMYULLEAN_DONE_GRAPH.md +++ /dev/null @@ -1,395 +0,0 @@ -# Native EVMYulLean Definition-Of-Done Graph - -This graph ranks the remaining work for making native EVMYulLean the -authoritative Verity runtime target. The ordering combines dependency pressure -and urgency: - -- **P0**: on the critical path to flipping the public theorem target. -- **P1**: required before the transition should be declared complete, but can - proceed in parallel with the P0 proof path. -- **P2**: cleanup after the native path is authoritative. - -The finish line is: - -```text -source DSL --> CompilationModel / generated Yul --> EVMYulLean native Yul runtime --> projected observable result --> public end-to-end theorem -``` - -The native EVMYulLean runtime is the sole theorem-facing Yul semantics for the -public end-to-end claim; historical interpreter scaffolding is not a regression -oracle for this boundary. - -## Critical Path - -```text -N0 generated-fragment contract - -> N1 native state/environment bridge - -> N2 native result projection - -> N3 native dispatcher skeleton - -> N4 body temp/write preservation - -> N5 generated statement/expression native equivalence - -> N6 whole runtime native/EVMYulLean agreement - -> N7 fuel adequacy / theorem-facing fuel story - -> N8 public Layer 3 theorem flip - -> N10 CI/docs enforcement -``` - -Parallel P1 work: - -```text -N9 issue-scope semantic closure - -> N8 public Layer 3 theorem flip -``` - -Post-flip P2 work: - -```text -N8 public Layer 3 theorem flip - -> N11 interpreter demotion/removal plan -``` - -## Ranked Nodes - -### N0: Generated-Fragment Contract - -- **Urgency**: P0 -- **Depends on**: none -- **Blocks**: N1, N3, N4, N5, N6 -- **Status**: partially implicit in existing lowering/proof modules. Generated - dispatcher selector lookup now has native-lowering specializations - `lowerSwitchCasesNativeWithSwitchIds_find?_some_of_find_function` and - `lowerSwitchCasesNativeWithSwitchIds_find?_none_of_find_function`, bridging - `IRFunction` selector hits and misses to lowered native case lookup facts. - The actual `buildSwitch` source case list is named by - `buildSwitchSourceCases`, proved equal to `switchCases` by - `buildSwitchSourceCases_eq_switchCases`, and consumed directly by - `lowerSwitchCasesNativeWithSwitchIds_buildSwitch_find?_some_of_find_function` - and `lowerSwitchCasesNativeWithSwitchIds_buildSwitch_find?_none_of_find_function`. - The singleton dispatcher-only runtime lowering boundary now lives in the - native harness as `lowerRuntimeContractNative_single_stmt_eq_lowerStmtsNative`. - Helper-free emitted runtime lowering is packaged by - `lowerRuntimeContractNative_emitYul_noMapping_noInternals_noFallback_noReceive`, - with the compiled supported no-mapping wrapper - `lowerRuntimeContractNative_of_compile_ok_supported_noMapping`. The generated - mapping-helper lowering is named by `nativeMappingSlotFunctionDefinition` and - `lowerFunctionDefinitionNativeWithReserved_mappingSlotFuncAt_zero`, pinning - the concrete native EVMYulLean function body for `mappingSlot` at scratch - base zero. Mapping-enabled emitted-runtime lowering is now packaged by - `lowerRuntimeContractNative_emitYul_mapping_noInternals_noFallback_noReceive_reserved` - and the compiled supported wrapper - `lowerRuntimeContractNative_of_compile_ok_supported_mapping_reserved`; the - dispatcher is lowered under the full emitted-runtime reserved-name context so - native switch temporary allocation remains faithful. Successful full native - lowering is now peeled back to its concrete dispatcher lowering by - `lowerRuntimeContractNative_emitYul_noMapping_ok_dispatcher`, - `lowerRuntimeContractNative_of_compile_ok_supported_noMapping_ok_dispatcher`, - `lowerRuntimeContractNative_emitYul_mapping_ok_dispatcher_reserved`, and - `lowerRuntimeContractNative_of_compile_ok_supported_mapping_ok_dispatcher_reserved`. - EndToEnd also - names the positive/projected no-mapping dispatcher-statement wrappers - `nativeIRRuntimeMatchesIR_of_compiled_generated_dispatcherStmts_positive_body_closure_noMapping` - and - `nativeIRRuntimeMatchesIR_of_compiled_generated_dispatcherStmts_project_body_closure_noMapping`, - plus their `_ofIR_environment` and `_ofIR_globalDefaults` variants and the - corresponding direct - `layer3_contract_preserves_semantics_native_of_compiled_generated_dispatcherStmts_*` - and `layers2_3_ir_matches_native_evmYulLean_of_generated_dispatcherStmts_*` - wrappers. The same native dispatcher-statement surface now exists for - mapping-enabled generated runtimes under the `_mapping_reserved` names, - including `nativeIRRuntimeMatchesIR_of_compiled_generated_dispatcherStmts_*`, - `layer3_contract_preserves_semantics_native_of_compiled_generated_dispatcherStmts_*`, - and `layers2_3_ir_matches_native_evmYulLean_of_generated_dispatcherStmts_*` - `_ofIR_environment` / `_ofIR_globalDefaults` variants. The - `nativeIRRuntimeMatchesIR_of_compiled_generated_lowered_runtime_dispatcherStmts_*` - wrappers compose successful full runtime lowering with the concrete - dispatcher-statement surfaces, keeping callers at the emitted-runtime - boundary while the exact dispatcher lowering is extracted internally. The - `layer3_contract_preserves_semantics_native_of_compiled_generated_lowered_runtime_dispatcherStmts_*` - wrappers lift that same full-runtime boundary to Layer 3, and - `layers2_3_ir_matches_native_evmYulLean_of_generated_lowered_runtime_dispatcherStmts_*` - lifts it to the Layers 2+3 composition. - Generic `.block` lowering shape also lives in the native harness via - `lowerStmtsNative_single_block_ok_singleton` and - `lowerStmtsNative_block_stmts_eq`; let/if/switch dispatcher statement peels - are named by `lowerStmtsNativeWithSwitchIds_let_head_eq`, - `lowerStmtsNativeWithSwitchIds_if_head_eq`, and - `lowerStmtsNativeWithSwitchIds_singleton_switch_eq`; default-revert switch - lowering is pinned by `lowerStmtsNativeWithSwitchIds_revert_zero_zero`, - `lowerStmtsNativeWithSwitchIds_singleton_switch_revert_default_eq`, and - `lowerStmtsNativeWithSwitchIds_singleton_switch_revert_default_eq_sourceLowered`. - The combined no-fallback/no-receive generated dispatcher lowering shape is - exposed as `buildSwitch_noFallback_noReceive_lowered_inner_sourceLowered`; - its selector hit/miss lookup companions are - `buildSwitch_noFallback_noReceive_lowered_inner_find?_some_of_find_function` - and `buildSwitch_noFallback_noReceive_lowered_inner_find?_none_of_find_function`. -- **Definition of done**: - - The compiler-emitted runtime Yul fragment is explicitly characterized. - - Allowed statements, expressions, builtins, helper functions, dispatcher - shape, storage layout, temp naming, and freshness guarantees are named. - - Unsupported native/runtime features are either syntactically excluded or - fail closed before theorem claims rely on them. -- **Verification**: - - `python3 scripts/check_native_transition_doc.py` - - Lean checks for `Compiler/Proofs/YulGeneration/Backends/EvmYulLeanNativeLowering.lean` - and `EvmYulLeanNativeHarness.lean`. - -### N1: Native State And Environment Bridge - -- **Urgency**: P0 -- **Depends on**: N0 -- **Blocks**: N5, N6, N8 -- **Status**: selector/calldata, sender/source/current address, callvalue, - timestamp, number, storage seeding, and storage projection have bridge - coverage. `chainid` and `blobbasefee` fail closed unless representable. - Header-derived builtins without Verity transaction fields now also fail - closed on selected native runtime paths. - `validateNativeRuntimeEnvironment_ofIR_representableEnvironment` and - `validateNativeRuntimeEnvironment_ofIR_globalDefaults` package the common - `YulTransaction.ofIR` validation cases once unsupported selected-path header - builtin use has been ruled out. EndToEnd mirrors the global-default case for - the compiled native positive and projected-result seams. - `generatedRuntimeNativeFragment_of_compile_ok_supported_safe` and - `validateGeneratedRuntimeNativeFragment_of_compile_ok_supported_safe` close - the executable generated-runtime fragment validator for supported compiled - contracts. -- **Definition of done**: - - Every environment field reachable from generated Yul is bridged, explicitly - defaulted under a theorem invariant, or rejected fail-closed. - - Static-call permissions, external call environment, returndata, gas, and - block-header fields have explicit scope decisions. - - State bridge lemmas cover the theorem-facing fields used by generated code. -- **Verification**: - - `lake build Compiler.Proofs.YulGeneration.Backends.EvmYulLeanStateBridge` - - `lake build Compiler.Proofs.YulGeneration.Backends.EvmYulLeanNativeHarness` - -### N2: Native Result Projection - -- **Urgency**: P0 -- **Depends on**: N1 -- **Blocks**: N6, N8 -- **Status**: projection coverage exists for success, stop, return, revert, - hard native errors, storage rollback, logs, and storage projection. The - generic - `exec_lowerNativeSwitchBlock_selector_find_none_with_revert_default_projectResult_eq` - lemma packages generated selector-miss native `Revert` execution with the - exact projected rollback result for arbitrary one-word selector tags. The - generic - `exec_lowerNativeSwitchBlock_selector_find_hit_error_projectResult_eq` lemma - packages selector-hit native halt/error execution with the selected body's - projected result at the full lowered-switch boundary; its store-parametric - companion - `exec_lowerNativeSwitchBlock_selector_find_hit_error_store_projectResult_eq` - covers selector switches entered after earlier dispatcher-local bindings. - The selector-miss companion - `exec_lowerNativeSwitchBlock_selector_find_none_with_revert_default_store_projectResult_eq` - gives the same exact rollback package for store-parametric miss/default - paths. The block-level - `exec_block_lowerNativeSwitchBlock_revert_default_hasSelectorState_projectResult_eq` - wrapper lifts that package through the generated dispatcher-local - `__has_selector` binding shape, while - `exec_block_lowerNativeSwitchBlock_selector_find_hit_hasSelectorState_error_projectResult_eq` - packages selector-hit halt/error projection through the same block shape. - Selector-hit normal-success projection is now packaged by - `exec_lowerNativeSwitchBlock_selector_find_hit_ok_projectResult_eq`, - `exec_lowerNativeSwitchBlock_selector_find_hit_ok_store_projectResult_eq` - and its dispatcher-block companion - `exec_block_lowerNativeSwitchBlock_selector_find_hit_hasSelectorState_ok_projectResult_eq`. - The final direct dispatcher-match surface also has the endpoint-agnostic - `nativeDispatcherExecMatchesIRPositive_of_project_eq_match`, which lets a - generated runtime proof discharge the positive native-vs-IR target from one - projected native `YulResult` equality plus the observable IR match. The - corresponding runtime lifts are named by - `nativeIRRuntimeMatchesIR_of_lowered_dispatcherExec_project_eq_match` and - `nativeIRRuntimeMatchesIR_of_generated_lowered_dispatcherExec_project_eq_match`. - EndToEnd now mirrors this as compiled-contract projected-result and - environment-closure seams: - `nativeIRRuntimeMatchesIR_of_compiled_generated_lowered_dispatcherExec_project_eq_match`, - `nativeIRRuntimeMatchesIR_of_compiled_generated_lowered_dispatcherExec_project_body_closure`, - `nativeIRRuntimeMatchesIR_of_compiled_generated_lowered_dispatcherExec_project_body_closure_ofIR_environment`, - `nativeIRRuntimeMatchesIR_of_compiled_generated_lowered_dispatcherExec_project_body_closure_ofIR_globalDefaults`, - `layer3_contract_preserves_semantics_native_of_compiled_generated_dispatcherExec_project_external_bodies_match`, - `layer3_contract_preserves_semantics_native_of_compiled_generated_dispatcherExec_project_body_closure`, - `layers2_3_ir_matches_native_evmYulLean_of_generated_dispatcherExec_project_external_bodies_match`, - `layers2_3_ir_matches_native_evmYulLean_of_generated_dispatcherExec_project_match`, - `layers2_3_ir_matches_native_evmYulLean_of_generated_dispatcherExec_project_body_closure`, - `layers2_3_ir_matches_native_evmYulLean_of_generated_dispatcherExec_project_body_closure_ofIR_environment`, - and `layers2_3_ir_matches_native_evmYulLean_of_generated_dispatcherExec_project_body_closure_ofIR_globalDefaults`. -- **Definition of done**: - - Projection lemmas cover every native halt/error/result used by generated - runtime execution. - - Revert/error rollback behavior is theorem-facing, not only tested. - - Multiword/non-word-aligned return behavior is either fully modeled or - restricted by generated-fragment invariants. -- **Verification**: - - `lake build Compiler.Proofs.YulGeneration.Backends.EvmYulLeanNativeHarness` - -### N3: Native Dispatcher Skeleton - -- **Urgency**: P0 -- **Depends on**: N0, N1 -- **Blocks**: N4, N6 -- **Status**: selector temp initialization, matched flag initialization, - guarded case/default execution, fuel-parametric case chains, lookup-driven - hit/miss lemmas, and raw lowered switch block bridge lemmas are in progress. -- **Definition of done**: - - The lowered dispatcher switch executes the same selected/default branch as - the EVMYulLean fuel wrapper for every generated dispatcher shape. - - The result is stated at the raw `lowerNativeSwitchBlock` boundary and at the - higher generated dispatcher boundary. -- **Verification**: - - `lake build Compiler.Proofs.YulGeneration.Backends.EvmYulLeanNativeHarness` - - `python3 scripts/check_proof_length.py` - - `lake env lean PrintAxioms.lean` - -### N4: Body Temp/Write Preservation - -- **Urgency**: P0, highest current bottleneck -- **Depends on**: N0, N3 -- **Blocks**: N5, N6 -- **Status**: `NativeBlockPreservesWord`, singleton/block-lift/if composition - including condition evaluations that preserve the tracked word, list-level - composition from per-statement preservation, selected freshness - projection lemmas, assignment constructors for literal/hex/identifier/string - RHS forms, generated `let` constructors for omitted, variable, and literal - initializers, generated `let` primitive-call constructors when argument - evaluation and the primitive preserve the tracked word, generated `let` and - expression-statement user-call wrappers when argument evaluation and the call - preserve the tracked word, read-only primitive preservation wrappers for - generated comparison, arithmetic, signed arithmetic, bitwise, shift, - switch-guard, calldata-size, - calldata/memory reads, callvalue, address/caller, timestamp/number, and - `sload` RHS forms, expression/argument-list preservation constructors for variables, - literals, primitive and user calls, and argument-list cons/nil composition, concrete - generated expression-statement constructors for `mstore` and `sstore` when - argument evaluation preserves the tracked word, and empty-store plus store-parametric - selector-hit freshness switch execution exist; general preservation from - `nativeStmtsWriteNames` freshness is not complete for all generated statement - families. -- **Definition of done**: - - If a generated native body does not write a dispatcher temp, native - execution preserves that temp. - - The theorem composes for selected switch bodies and default bodies. - - The matched flag remains stable after case body execution, so default - execution is skipped exactly when it should be. -- **Verification**: - - `lake build Compiler.Proofs.YulGeneration.Backends.EvmYulLeanNativeHarness` - -### N5: Generated Statement/Expression Native Equivalence - -- **Urgency**: P0 -- **Depends on**: N0, N1, N2, N4 -- **Blocks**: N6 -- **Status**: selected expression and lowering equations exist, but full - generated-fragment statement/block equivalence is incomplete. -- **Definition of done**: - - Generated `let`, assignment, expression statements, blocks, conditionals, - switches, loops if emitted, calls, storage operations, memory operations, - events, return/stop/revert, and ABI snippets agree with the oracle. - - Generated expression equivalence covers constants, locals, arithmetic, - comparisons, calldata, memory, storage, keccak/slot computations, selector - extraction, and supported builtins. -- **Verification**: - - `lake build Compiler.Proofs.YulGeneration.Backends.EvmYulLeanNativeLowering` - - `lake build Compiler.Proofs.YulGeneration.Backends.EvmYulLeanNativeHarness` - -### N6: Whole Runtime Native/Interpreter Agreement - -- **Urgency**: P0 -- **Depends on**: N1, N2, N3, N4, N5 -- **Blocks**: N7, N8 -- **Status**: theorem seams exist; the whole-runtime agreement theorem is not - complete. -- **Definition of done**: - - For compiler-generated runtime Yul, native `callDispatcher` execution - agrees with the current EVMYulLean-backed public semantic path on the - observable result. - - The theorem covers selected public function dispatch, default dispatch, - storage, logs, return/revert/error behavior, and generated helper calls. -- **Verification**: - - `lake build Compiler.Proofs.EndToEnd` - - `lake env lean PrintAxioms.lean` - -### N7: Fuel Story - -- **Urgency**: P0 -- **Depends on**: N6 -- **Blocks**: N8 -- **Status**: several fuel-parametric switch lemmas exist; public theorem fuel - policy is unresolved. -- **Definition of done**: - - The public theorem is either fuel-parametric, backed by a generated-code - fuel lower bound, or uses a total wrapper with a proved sufficient default. - - Fuel exhaustion cannot masquerade as semantic disagreement. -- **Verification**: - - `lake build Compiler.Proofs.EndToEnd` - -### N8: Public Theorem Flip - -- **Urgency**: P0 -- **Depends on**: N6, N7, N9 -- **Blocks**: N10, N11 -- **Status**: done. The public path targets native EVMYulLean dispatcher - execution rather than the removed backend-parameterized interpreter path. -- **Definition of done**: - - Layer 3 theorem statements and generated proof plumbing target - `interpretIRRuntimeNative` or an equivalent native `callDispatcher` wrapper. - - The theorem no longer depends on an alternate interpreter for the public - semantic claim. -- **Verification**: - - grep for the former backend-interpreter name in `Compiler/Proofs/EndToEnd.lean` - and confirm it has no matches. - - `lake build Compiler.Proofs.EndToEnd` - -### N9: Issue-Scope Semantic Closure - -- **Urgency**: P1 -- **Depends on**: N0, N1, N5 -- **Blocks**: N8 as a release-quality gate -- **Status**: partial progress exists for environment reads, mapping structs, - packed members, overload identity, typed-IR ambiguity rejection, and direct - internal calls. -- **Definition of done**: - - Mapping-struct and packed-member behavior is covered by source, generated - Yul, native execution, and proof-level preservation. - - Signature/selector dispatch is fully selector-based where ABI dispatch is - relevant. - - External calls, returndata, and static-call behavior are either proved or - explicitly out of the generated native fragment. -- **Verification**: - - Existing property/differential tests for storage, overloads, external calls, - and environment features. - - `python3 scripts/check_native_transition_doc.py` - -### N10: CI And Docs Enforcement - -- **Urgency**: P0 after N8 -- **Depends on**: N8 -- **Blocks**: transition declaration -- **Status**: transition doc guards exist and now pin additional fail-closed - environment boundary behavior. -- **Definition of done**: - - CI fails if the public theorem target drifts back to the interpreter. - - CI keeps transition docs, native smoke tests, axiom report, and generated - fragment boundary in sync. - - Axioms report is updated and reviewed after the theorem flip. -- **Verification**: - - `python3 scripts/check_native_transition_doc.py` - - `python3 scripts/check_proof_length.py` - - `lake env lean PrintAxioms.lean` - -### N11: Interpreter Demotion - -- **Urgency**: P2 -- **Depends on**: N8, N10 -- **Blocks**: eventual cleanup only -- **Status**: not started. -- **Definition of done**: - - The former Verity-side interpreter path is absent from the checked-in public proof tree. - - Historical comparisons, if reintroduced, stay outside the authoritative - public semantic-preservation path. -- **Verification**: - - Documentation and CI checks name the native path as authoritative. diff --git a/docs/archive/NATIVE_EVMYULLEAN_FULL_TRANSITION_DONE.md b/docs/archive/NATIVE_EVMYULLEAN_FULL_TRANSITION_DONE.md deleted file mode 100644 index f57a3ccbf5..0000000000 --- a/docs/archive/NATIVE_EVMYULLEAN_FULL_TRANSITION_DONE.md +++ /dev/null @@ -1,362 +0,0 @@ -# Full Native EVMYulLean Transition: Definition Of Done - -This document records the completed transition from Verity's former Yul -interpreter path to native EVMYulLean execution as the authoritative -compiler-verification target. - -The SimpleStorage native theorem discharges its retrieve, store, and selector -bridge cases, and the public generated-dispatcher theorem family targets native -EVMYulLean dispatcher execution. Removed transition evidence used to record a -Verity-side Yul statement-interpreter path through the EVMYulLean-backed builtin -backend. - -The removed backend-fuel theorem family marks the old shape that has now been -deleted. The public compiler-correctness theorem targets native EVMYulLean -execution directly through `EvmYul.Yul.callDispatcher`; that condition is -satisfied on `main`. - -## Current Baseline - -The current baseline is: - -- Layer 2 proves semantic preservation from supported `CompilationModel` - programs to IR. -- Public EndToEnd proofs now target - `Compiler.Proofs.YulGeneration.Backends.Native.interpretIRRuntimeNative`, - which executes through native EVMYulLean. Helper-free and mapping-helper - generated-dispatcher wrappers consume concrete dispatcher lowering and build - full emitted-runtime native lowering internally. -- Lower Layer 3 transition modules no longer contain the old builtin - comparison path or the backend-parameterized builtin wrapper. -- `simpleStorage_endToEnd_native_evmYulLean` is a concrete native theorem over - the direct projected `EvmYul.Yul.callDispatcher` result, with no public - retrieve-hit, store-hit, or selector-miss bridge premises. -- The active compiler stack has zero project-level Lean axioms and zero `sorry` - placeholders. - -## Final Architecture - -The completed proof path must be: - -```text -CompilationModel - -> IRContract - -> emitted Verity Yul runtime - -> EVMYulLean YulContract - -> EvmYul.Yul.callDispatcher - -> projected observable result - -> public end-to-end theorem -``` - -Differential tests may still compare generated code against external execution -engines, but the public semantic-preservation proof path must not depend on an -alternate Verity Yul interpreter. - -## Non-Negotiable Completion Criteria - -The transition is done only when all criteria in this section are true on -`main`. - -### 1. Public Theorem Target - -- The generic Layer 3 and end-to-end compiler-correctness theorem statements - target native EVMYulLean execution, not the removed backend-parameterized - interpreter path. -- The theorem-facing native target is either `Native.interpretIRRuntimeNative` - or a narrower wrapper that internally lowers to EVMYulLean and executes via - `EvmYul.Yul.callDispatcher`. -- No public theorem keeps a native-vs-alternate-interpreter agreement premise such - as `nativeIRRuntimeAgreesWithEvmYulLeanFuelWrapper`, - `nativeCallDispatcherAgreesWithEvmYulLeanFuelWrapper`, or an equivalent bridge - assumption. -- Any remaining comparison with non-proof execution engines is explicitly - marked as differential testing, not as the authoritative proof target. - -### 2. Generated Fragment Characterization - -- The compiler-emitted Yul fragment is represented by named syntactic - predicates or theorem hypotheses that can be generated from the compiler - output without manual contract-specific proof work. -- The characterization covers generated statements, expressions, helper - functions, dispatcher shape, storage layout, memory/return ABI snippets, - event/log snippets, selector extraction, callvalue guards, calldata guards, - and native switch temporaries. -- Unsupported constructs fail closed before the public theorem claims semantic - preservation. They must not silently fall back to an alternate execution - semantics. -- The generated-fragment contract is tested by CI so new emitted Yul forms - cannot bypass the native proof surface. - -### 3. Native State Bridge - -- Every transaction, environment, and state field reachable from generated Yul - has one of three statuses: - - bridged with a theorem, - - explicitly constrained by a theorem-facing invariant, or - - rejected fail-closed before execution. -- The bridge covers selector/calldata bytes, calldata word reads, caller, - source, current address, callvalue, timestamp, block number, supported chain - and blob fields, storage, transient storage if emitted, memory, returndata, - logs, and rollback behavior. -- State projection uses bounded EVM-compatible storage keys and words. No proof - relies on Nat slot injectivity after coercion to 256-bit EVM keys. -- Any EVMYulLean fork divergence required by the state bridge is either - upstreamed or documented in the fork audit with a non-semantic explanation. - -### 4. Native Statement And Expression Semantics - -- For every statement and expression form generated by the compiler, there is a - theorem proving native EVMYulLean execution agrees with the source/IR - semantics used by the compiler proof. -- Coverage includes literals, locals, assignments, blocks, conditionals, - switches, loops if emitted, function/helper calls, arithmetic, comparisons, - bit operations, calldata operations, storage operations, memory operations, - mapping-slot helpers, returns, stops, reverts, and logs. -- Dispatcher execution is proved for selector hits, selector misses, default - arms, duplicate-case fail-closed behavior, matched-flag preservation, and - generated native switch temporary freshness. -- External calls, static calls, returndata, and gas-sensitive behavior are - either fully modeled and proved or syntactically excluded from the generated - native fragment used by the public theorem. - -### 5. Result Projection - -- Native success, stop, return, revert, and hard-error results are projected to - the theorem-facing observable result with proved lemmas. -- Projection covers success flags, return values, final storage on observable - slots, final mappings if still present in the result shape, event/log - histories, rollback semantics, and missing-account/missing-slot defaults. -- Non-word-sized return or revert buffers are either modeled faithfully or - excluded by a generated-fragment theorem. -- Projection lemmas are not contract-specific; concrete contract theorems may - instantiate them, but the generic transition cannot depend on SimpleStorage - closed forms. - -### 6. Fuel And Termination Story - -- Fuel cannot appear as an unexplained public assumption. -- The public theorem uses one of these designs: - - a fuel-parametric theorem with explicit monotonicity/sufficiency facts, - - a generated-code fuel lower bound proved from the emitted runtime, or - - a total wrapper that chooses and proves a sufficient fuel internally. -- Fuel exhaustion must project as a closed failure only when the theorem - explicitly reasons about that failure. It must not be confused with semantic - disagreement. - -### 7. Proof Cleanliness - -- No `sorry`, `admit`, `axiom`, or theorem declaration marked as an assumption - is introduced for the transition. -- `AXIOMS.md`, generated axiom reports, and `PrintAxioms.lean` agree that the - active compiler stack has zero project-level Lean axioms. -- The public native theorem's `#print axioms` output contains only Lean - foundational axioms such as `propext`, `Classical.choice`, - `Lean.ofReduceBool`, or `Quot.sound`. -- There are no hidden local assumptions that reintroduce alternate interpreter - correctness, Nat storage-slot injectivity, unbounded storage values, or - unconstrained environment defaults. - -### 8. Documentation And Trust Boundary - -- `README.md`, `AUDIT.md`, `TRUST_ASSUMPTIONS.md`, and - `docs/VERIFICATION_STATUS.md` describe native EVMYulLean as the - authoritative semantic target. -- Removed alternate-interpreter paths do not appear in the checked-in proof tree - or theorem-facing reports. -- EVMYulLean fork audit artifacts and capability reports are regenerated and - checked. -- The transition docs name all remaining excluded features and link them to - issue-tracked scope decisions. - -### 9. CI Enforcement - -- CI fails if the public compiler theorem drifts away from the native - EVMYulLean dispatcher path. -- CI checks the generated-fragment boundary, native transition docs, bridge - coverage reports, fork audit artifacts, axiom report, proof-length metadata, - and native smoke/regression tests. -- The following local validations pass before merge: - -```bash -lake build Compiler.Proofs.YulGeneration.Backends.EvmYulLeanNativeHarness -lake build Compiler.Proofs.YulGeneration.Backends.EvmYulLeanNativeLowering -lake build Compiler.Proofs.EndToEnd -lake build Compiler -lake build Contracts -lake build PrintAxioms -python3 scripts/generate_print_axioms.py --check -python3 scripts/check_native_transition_doc.py -python3 scripts/check_bridge_coverage_sync.py -python3 scripts/check_evmyullean_capability_boundary.py -make check -``` - -### 10. Merge Readiness - -- Every PR in the transition stack is based on `main` or the PR directly below - it. -- Required-equivalent GitHub checks are green on the head commit. -- Cursor Bugbot, Codex, or maintainer review threads on the head commit are - resolved. -- Generated files are produced by scripts, not hand-edited. -- Branches are pushed with `git push --force-with-lease` when rebasing is - necessary; plain `--force` is never used. - -## Incremental PR Shape - -Each PR should move exactly one durable boundary: - -1. Native generated-fragment predicate or checker. -2. Native state/environment bridge theorem family. -3. Native result projection theorem family. -4. Native statement/expression equivalence for one generated construct family. -5. Dispatcher/native-switch preservation. -6. Fuel sufficiency or fuel-parametric theorem layer. -7. Fuel adequacy and native execution modularity for generated Yul. -8. Public theorem target maintenance. -9. CI/doc/trust-boundary cleanup after proof-boundary changes. -10. Keep removed interpreter/adapter surfaces out of the public proof path. - -Do not mix proof work with unrelated cleanup. Do not reintroduce alternate -interpreter paths as proof authority. - -## Reusable Agent Prompt - -Use this prompt for repeated focused sessions on the transition. Set -`VERITY_REPO` to the checkout path and `VERITY_MEMORY_DIR` to the local memory -directory before using it. - -```text -You are working on the Verity repo native EVMYulLean transition. - -Repo: - $VERITY_REPO - -Memory: - $VERITY_MEMORY_DIR - -Before doing anything: - 1. cd "$VERITY_REPO" - 2. source "$HOME/.elan/env" 2>/dev/null || export PATH="$HOME/.elan/bin:$PATH" - 3. Read: - "$VERITY_MEMORY_DIR"/MEMORY.md - any native EVMYulLean transition memory files - any IRStorageWord / IRStorageSlot memory files - docs/archive/NATIVE_EVMYULLEAN_DONE_GRAPH.md - docs/archive/NATIVE_EVMYULLEAN_FULL_TRANSITION_DONE.md - 4. git fetch --all --prune - 5. Inspect current open PRs and relevant branches with gh. - -Goal: - Keep native EVMYulLean as the authoritative public compiler-verification - target. - -Current architecture: - The public proof path runs through: - - Native.interpretRuntimeNative - Native.interpretIRRuntimeNative - EvmYul.Yul.callDispatcher - - The removed backend-fuel path must not be reintroduced as a theorem-facing - authority. - -Definition of done: - Follow docs/archive/NATIVE_EVMYULLEAN_FULL_TRANSITION_DONE.md exactly. Do not claim - the transition is complete until every criterion in that document holds. - -Per-session rule: - Pick the bottom-most open PR in the native EVMYulLean transition stack that is - not merge-ready and make it merge-ready. If no PR exists, create the smallest - useful PR that advances one durable boundary from the "Incremental PR Shape" - section of docs/archive/NATIVE_EVMYULLEAN_FULL_TRANSITION_DONE.md. - -Preferred work order: - 1. Generated-fragment characterization and fail-closed checks. - 2. Native state/environment bridge theorem families. - 3. Native result projection theorem families. - 4. Native statement/expression equivalence by generated construct family. - 5. Dispatcher/native-switch preservation and temporary freshness. - 6. Fuel sufficiency or fuel-parametric preservation. - 7. Generic generated-runtime native agreement. - 8. Public Layer 3 and EndToEnd theorem target flip. - 9. CI, docs, trust-boundary update. - 10. Keep removed interpreter/adapter surfaces out of the public proof path. - -Hard constraints: - - Do not add public theorem assumptions merely to make proofs pass. - - Do not use sorry, admit, axioms, unchecked declarations, or hidden local - assumptions. - - Do not route the final public proof through an alternate Yul interpreter. - - Do not weaken generated-fragment predicates to include constructs that the - native proof does not actually cover. - - Do not paper over environment, storage, memory, returndata, logs, gas, or - external-call gaps with default values unless a theorem-facing invariant or - fail-closed validator justifies the default. - - Do not change CI workflows, branch protection, audit/trust docs, or public - theorem hypotheses without a precise reason in the PR. - - Do not hand-edit generated artifacts. - - Do not use plain git push --force; use git push --force-with-lease. - -Validation for proof PRs: - Run the smallest relevant Lean target first, then the broader stack: - - lake build Compiler.Proofs.YulGeneration.Backends.EvmYulLeanNativeHarness - lake build Compiler.Proofs.YulGeneration.Backends.EvmYulLeanNativeLowering - lake build Compiler.Proofs.EndToEnd - lake build Compiler - lake build Contracts - lake build PrintAxioms - python3 scripts/generate_print_axioms.py --check - python3 scripts/check_native_transition_doc.py - python3 scripts/check_bridge_coverage_sync.py - python3 scripts/check_evmyullean_capability_boundary.py - make check - -Validation for docs-only PRs: - Run: - - python3 scripts/check_native_transition_doc.py - python3 scripts/check_docs_workflow_sync.py - make check - -Merge-readiness: - A PR is ready only when local validation passes, required-equivalent GitHub - checks are green on the head commit, and all Bugbot/Codex/maintainer review - threads are resolved. Merge with gh pr merge --squash --delete-branch only - after those conditions hold. - -Memory update rule: - At the end of the session, update memory with stable findings only: - - merged PRs and commit SHAs - - durable proof patterns - - accepted scope constraints - - real blockers - - final architecture decisions - -Work style: - Keep working until the selected PR is merge-ready or a real blocker is - precisely documented. If a proof is structurally impossible under the current - theorem surface, stop adding code, write a precise memory note, and explain - the obstruction. -``` - -## Completion Checklist - -Before declaring the transition complete, verify these exact facts: - -```bash -! rg "nativeIRRuntimeAgreesWithEvmYulLeanFuelWrapper|nativeCallDispatcherAgreesWithEvmYulLeanFuelWrapper" Compiler/Proofs/EndToEnd.lean -! rg "\\bsorry\\b|\\badmit\\b|^axiom " Compiler Verity Contracts -python3 scripts/check_axioms.py -python3 scripts/generate_print_axioms.py --check -lake build Compiler.Proofs.EndToEnd -lake build Compiler -lake build Contracts -lake build PrintAxioms -make check -``` - -The first two `rg` commands should either find no public-theorem dependency on -the old backend-fuel path, or should find only documentation/comments and -historical lemmas explicitly excluded from the public semantic target. diff --git a/docs/archive/README.md b/docs/archive/README.md deleted file mode 100644 index 8fede1f957..0000000000 --- a/docs/archive/README.md +++ /dev/null @@ -1,12 +0,0 @@ -# Archived Design Notes - -This directory keeps historical implementation plans and completion notes that -are useful for audit history but are not current source-of-truth documentation. - -Current proof, trust, and architecture status lives in: - -- [`docs/VERIFICATION_STATUS.md`](../VERIFICATION_STATUS.md) -- [`docs/INTERPRETER_FEATURE_MATRIX.md`](../INTERPRETER_FEATURE_MATRIX.md) -- [`docs/ARCHITECTURE.md`](../ARCHITECTURE.md) -- [`docs/ROADMAP.md`](../ROADMAP.md) - diff --git a/docs/parity/README.md b/docs/parity/README.md deleted file mode 100644 index c3e831bf37..0000000000 --- a/docs/parity/README.md +++ /dev/null @@ -1,41 +0,0 @@ -# Solidity parity gap maps - -Per-contract gap maps between production Solidity and the `verity_contract` -EDSL. Each file lists every top-level construct (state variables, structs, -events, errors, modifiers, functions) with one of: - -- ✅ expressible in `verity_contract` today -- 🚧 expressible with a workaround (workaround noted) -- ❌ requires a new EDSL feature (feature named) - -The maps are used to (a) prioritize feature work in issue -[#1724](https://github.com/lfglabs-dev/verity/issues/1724) and (b) drive a -planned nightly `contracts-smoke` CI job (not yet implemented) that will -compile each reference contract through Verity and assert no rows regress. - -## Contents - -- [`lido.md`](lido.md) — Lido StakingRouter v3 (Solidity 0.8.25 / 0.4.24) -- [`erc4337.md`](erc4337.md) — ERC-4337 v0.9 EntryPoint stack (eth-infinitism - reference implementation) -- [`morpho.md`](morpho.md) — Morpho Blue singleton (`morpho-org/morpho-blue`) - -## Convergent findings - -Across all three contracts (423 constructs scored), the same top-3 missing -EDSL features dominate: - -| Rank | Feature | Lido hits | 4337 hits | Morpho hits | -|---|---|---:|---:|---:| -| 1 | Narrow uints (`uint128`, `uint64`, `uint48`, `uint32`, `uint16`) | ~45 | 22 | ~15 | -| 2 | `abi.encode` / `encodePacked` / `encodeCall` codec | 6+ | 9 | 3 | -| 3 | First-class modifiers + inheritance (`is X, Y`) | ~30 | 8 | 4 | - -Landing these three lands every reference contract north of 70% ✅ in one -wave; individual contract-specific gaps (EIP-712 for 4337/Morpho, `enum` for -Lido, flashloan callback for Morpho) come next. - -## Updating a map - -Any PR that changes a `verity_contract` construct row (adds support, changes -signature, deletes) MUST update the corresponding row in these files. diff --git a/docs/parity/erc4337.md b/docs/parity/erc4337.md deleted file mode 100644 index a27d457a32..0000000000 --- a/docs/parity/erc4337.md +++ /dev/null @@ -1,370 +0,0 @@ -# ERC-4337 EntryPoint × Verity EDSL — parity gap map - -Scope: Solidity ↔ `verity_contract` EDSL coverage for the ERC-4337 v0.9 -EntryPoint stack the harness in -[`lfglabs-dev/erc4337-verity`](https://github.com/lfglabs-dev/erc4337-verity) -targets. That repo carries only Python glue, adapters, and a submodule at -`upstream/account-abstraction/` pinned to -[`eth-infinitism/account-abstraction@b36a1ed5`](https://github.com/eth-infinitism/account-abstraction/tree/b36a1ed52ae00da6f8a4c8d50181e2877e4fa410) -(tag `v0.9.0`), so this document scores that pinned commit as the source of -truth. - -Files scored (all under `contracts/`): - -| File | LOC | Role | -| --- | --- | --- | -| `core/EntryPoint.sol` | 1013 | Singleton entrypoint (>500 LOC → storage-layout + designated entrypoints only) | -| `core/StakeManager.sol` | 148 | Stake/deposit accounting mixed into EntryPoint | -| `core/NonceManager.sol` | 40 | 2-D nonce mapping | -| `core/BaseAccount.sol` | 166 | Abstract account base | -| `core/BasePaymaster.sol` | 144 | Abstract paymaster base | -| `core/UserOperationLib.sol` | 231 | Packing, hashing, offsets | -| `accounts/SimpleAccount.sol` | 127 | Reference singleton account | -| `interfaces/IEntryPoint.sol` | 261 | Errors, events, structs | -| `interfaces/IStakeManager.sol` | 120 | Errors, events, `DepositInfo` | -| `interfaces/IPaymaster.sol` | 63 | `PostOpMode` enum, paymaster ABI | -| `interfaces/PackedUserOperation.sol` | 46 | Wire struct | - -Legend: ✅ expressible in `verity_contract` today · 🚧 expressible with a -workaround (noted) · ❌ needs new EDSL work. Tracked in verity issue -[#2071](https://github.com/lfglabs-dev/verity/issues/2071); related umbrella -issues [#1724](https://github.com/lfglabs-dev/verity/issues/1724), -[#1982](https://github.com/lfglabs-dev/verity/issues/1982), -[#2002](https://github.com/lfglabs-dev/verity/issues/2002), -[#2060](https://github.com/lfglabs-dev/verity/issues/2060). - -## PackedUserOperation.sol - -| Construct | Signature | Verity | Notes | -| --- | --- | --- | --- | -| struct field | `address sender` | ✅ | | -| struct field | `uint256 nonce` | ✅ | | -| struct field | `bytes initCode` | 🚧 | Only via `calldata`; storage `bytes` still ❌ (see #1982). | -| struct field | `bytes callData` | 🚧 | Same. | -| struct field | `bytes32 accountGasLimits` (packed 2×uint128) | ✅ | Word storage OK; unpacking below is ❌. | -| struct field | `uint256 preVerificationGas` | ✅ | | -| struct field | `bytes32 gasFees` (packed 2×uint128) | ✅ | | -| struct field | `bytes paymasterAndData` | 🚧 | calldata-only. | -| struct field | `bytes signature` | 🚧 | calldata-only. | -| top-level struct type | `struct PackedUserOperation { ... }` (as calldata param) | 🚧 | Modelled as calldata bag; top-level *storage* struct still ❌. | - -## interfaces/IStakeManager.sol - -| Construct | Signature | Verity | Notes | -| --- | --- | --- | --- | -| error | `InvalidUnstakeDelay(uint256,uint256)` | ✅ | | -| error | `InvalidStake(uint256,uint256)` | ✅ | | -| error | `NotStaked(uint256,uint256,bool)` | ✅ | | -| error | `InsufficientDeposit(uint256,uint256)` | ✅ | | -| error | `StakeNotUnlocked(uint256,uint256)` | ✅ | | -| error | `WithdrawalNotDue(uint256,uint256)` | ✅ | | -| error | `StakeWithdrawalFailed(address,address,uint256,bytes)` | 🚧 | `bytes` in composite payload — needs #1982 codec. | -| error | `DepositWithdrawalFailed(address,address,uint256,bytes)` | 🚧 | Same. | -| event | `Deposited(address indexed, uint256)` | ✅ | | -| event | `Withdrawn(address indexed, address, uint256)` | ✅ | | -| event | `StakeLocked(address indexed, uint256, uint256)` | ✅ | | -| event | `StakeUnlocked(address indexed, uint256)` | ✅ | | -| event | `StakeWithdrawn(address indexed, address, uint256)` | ✅ | | -| struct | `DepositInfo { uint256; bool; uint112; uint32; uint48 }` | ❌ | Needs `uint112`, `uint32`, `uint48` + top-level packed storage struct (#2060). | -| struct | `StakeInfo { uint256, uint256 }` | ✅ | | -| fn | `getDepositInfo(address) view returns (DepositInfo)` | ❌ | Depends on struct above. | -| fn | `balanceOf(address) view returns (uint256)` | ✅ | | -| fn | `depositTo(address) payable` | ✅ | | -| fn | `addStake(uint32) payable` | ❌ | `uint32` parameter unsupported. | -| fn | `unlockStake()` | ✅ | | -| fn | `withdrawStake(address payable)` | ✅ | `payable` on address is a no-op at type level. | -| fn | `withdrawTo(address payable, uint256)` | ✅ | | - -## core/StakeManager.sol (concrete abstract) - -| Construct | Signature | Verity | Notes | -| --- | --- | --- | --- | -| storage | `mapping(address => DepositInfo) private deposits` | ❌ | Value type is packed struct (#2060). Workaround: split into parallel `mapping(address => uint256)`. | -| fn | `getDepositInfo(address) view returns (DepositInfo)` | ❌ | See struct. | -| fn | `_getStakeInfo(address) view returns (StakeInfo)` | 🚧 | Model as two-return `uint256`s. | -| fn | `balanceOf(address) view returns (uint256)` | ✅ | | -| receive | `receive() external payable` | ❌ | No `receive/fallback` — must expose an explicit `depositTo` entrypoint. | -| fn | `_incrementDeposit(address, uint256) returns (uint256)` | ✅ | | -| fn | `_tryDecrementDeposit(address, uint256) returns (bool)` | ✅ | | -| fn | `depositTo(address)` payable | ✅ | | -| fn | `addStake(uint32)` payable | ❌ | `uint32` arg. | -| fn | `unlockStake()` | 🚧 | Uses `uint48(block.timestamp)` cast — needs `uint48`; workaround: `uint256`. | -| fn | `withdrawStake(address payable)` | 🚧 | `.call{value}()`: OK in EDSL, no proof; `transfer/send` remain ❌. | -| fn | `withdrawTo(address payable, uint256)` | 🚧 | Same. | - -## core/NonceManager.sol - -| Construct | Signature | Verity | Notes | -| --- | --- | --- | --- | -| storage | `mapping(address => mapping(uint192 => uint256)) public nonceSequenceNumber` | ❌ | `mapping2` supported, but *key type `uint192`* is ❌. Workaround: `uint256` key + truncate. | -| fn | `getNonce(address, uint192) view returns (uint256)` | ❌ | `uint192` arg. | -| fn | `incrementNonce(uint192)` | ❌ | Same. | -| fn | `_validateAndUpdateNonce(address, uint256) returns (bool)` | ❌ | Uses `uint192(nonce >> 64)` and `uint64(nonce)` casts. | - -## interfaces/IEntryPoint.sol - -| Construct | Signature | Verity | Notes | -| --- | --- | --- | --- | -| event | `UserOperationEvent(bytes32 idx, address idx, address idx, uint256, bool, uint256, uint256)` | ✅ | | -| event | `AccountDeployed(bytes32 idx, address idx, address, address)` | ✅ | | -| event | `IgnoredInitCode(bytes32 idx, address idx, address)` | ✅ | | -| event | `EIP7702AccountInitialized(bytes32 idx, address idx, address idx)` | ✅ | | -| event | `UserOperationRevertReason(bytes32 idx, address idx, uint256, bytes)` | 🚧 | `bytes` payload. | -| event | `PostOpRevertReason(bytes32 idx, address idx, uint256, bytes)` | 🚧 | | -| event | `UserOperationPrefundTooLow(bytes32 idx, address idx, uint256)` | ✅ | | -| event | `BeforeExecution()` | ✅ | | -| event | `SignatureAggregatorChanged(address idx)` | ✅ | | -| error | `FailedOp(uint256, string)` | ❌ | Composite error with storage/memory `string`. Workaround: use `bytes32` code. | -| error | `InvalidBeneficiary(address)` | ✅ | | -| error | `FailedSendToBeneficiary(address, uint256, bytes)` | 🚧 | | -| error | `InternalFunction()` | ✅ | | -| error | `InvalidPaymasterData(uint256)` | ✅ | | -| error | `InvalidPaymaster(address)` | ✅ | | -| error | `FailedOpWithRevert(uint256, string, bytes)` | ❌ | `string` + `bytes` — needs full composite codec (#1982). | -| error | `PostOpReverted(bytes)` | 🚧 | | -| error | `SignatureValidationFailed(address)` | ✅ | | -| error | `SenderAddressResult(address)` | ✅ | | -| error | `DelegateAndRevert(bool, bytes)` | 🚧 | | -| struct | `UserOpsPerAggregator { PackedUserOperation[]; IAggregator; bytes }` | 🚧 | Only in calldata; storage form ❌. | -| struct | `ReturnInfo { 4× uint256, bytes }` | 🚧 | | -| fn | `handleOps(PackedUserOperation[] calldata, address payable)` | 🚧 | Body gaps drive the score; signature-wise fine. | -| fn | `handleAggregatedOps(UserOpsPerAggregator[] calldata, address payable)` | 🚧 | | -| fn | `getUserOpHash(PackedUserOperation calldata) view returns (bytes32)` | ❌ | Body needs EIP-712 domain separator + typed-data hash (#2002). | -| fn | `getCurrentUserOpHash() view returns (bytes32)` | ✅ | | -| fn | `getSenderAddress(bytes memory)` | ❌ | Requires `CREATE`/`CREATE2` codepath. | -| fn | `delegateAndRevert(address, bytes calldata)` | ❌ | `delegatecall` compile OK; the always-revert with `bytes` isn't provable. | -| fn | `senderCreator() view returns (ISenderCreator)` | 🚧 | Immutable of a contract type; treat as `address`. | - -## interfaces/IPaymaster.sol - -| Construct | Signature | Verity | Notes | -| --- | --- | --- | --- | -| enum | `PostOpMode { opSucceeded, opReverted, postOpReverted }` | ✅ | Native uint8-backed enum declarations, members, and checked casts (#2088). | -| fn | `validatePaymasterUserOp(PackedUserOperation calldata, bytes32, uint256) returns (bytes, uint256)` | 🚧 | `bytes` return only lands as raw memory. | -| fn | `postOp(PostOpMode, bytes calldata, uint256, uint256)` | ✅ | Enum arguments erase to `uint8` in the ABI. | - -## core/BasePaymaster.sol - -| Construct | Signature | Verity | Notes | -| --- | --- | --- | --- | -| storage | `IEntryPoint internal immutable _entryPoint` | 🚧 | As `address` immutable. | -| const | `PAYMASTER_VALIDATION_GAS_OFFSET = 20` (uint256) | ✅ | | -| const | `PAYMASTER_POSTOP_GAS_OFFSET = 36` | ✅ | | -| const | `PAYMASTER_DATA_OFFSET = 52` | ✅ | | -| error | `NotFromEntryPoint(address,address,address)` | ✅ | | -| error | `ERC165Error(address, bytes4)` | ❌ | `bytes4` payload. | -| error | `MustOverride()` | ✅ | | -| inheritance | `is IPaymaster, Stakeable` (via `Ownable2Step`) | 🚧 | Flattened single inheritance is supported; this multiple-inheritance chain still needs C3 linearization. | -| constructor | `constructor(IEntryPoint, address) Ownable(owner)` | 🚧 | Direct parent-constructor calls are supported; the remaining gap is the surrounding multiple-inheritance chain. | -| fn | `entryPoint() public view returns (IEntryPoint)` | 🚧 | Return as `address`. | -| fn | `_validateEntryPointInterface(IEntryPoint) internal` | ❌ | Uses `type(IEntryPoint).interfaceId` (`bytes4`) + `IERC165(...).supportsInterface`. | -| fn | `validatePaymasterUserOp(...) external` | 🚧 | Body dispatches to internal template; template itself is ❌ (abstract). | -| fn | `_validatePaymasterUserOp(...) internal virtual` | ❌ | Abstract → no `virtual`/override slot in EDSL. | -| fn | `postOp(PostOpMode,...) external` | ❌ | Enum arg. | -| fn | `_postOp(...) internal virtual` | ❌ | Abstract + enum. | -| fn | `deposit() public payable` | ✅ | | -| fn | `withdrawTo(address payable, uint256) public onlyOwner` | ✅ | Precondition-only user modifiers and inherited modifiers inline at compile time. | -| fn | `getDeposit() public view returns (uint256)` | ✅ | | -| fn | `_requireFromEntryPoint() internal` | ✅ | | - -## core/BaseAccount.sol - -| Construct | Signature | Verity | Notes | -| --- | --- | --- | --- | -| struct (calldata) | `Call { address, uint256, bytes }` | 🚧 | Calldata-only. | -| error | `ExecuteError(uint256, bytes)` | 🚧 | `bytes` payload. | -| error | `NotFromEntryPoint(address,address,address)` | ✅ | | -| fn | `getNonce() view returns (uint256)` | ✅ | | -| fn (abstract) | `entryPoint() view returns (IEntryPoint)` | 🚧 | | -| fn | `execute(address,uint256,bytes calldata)` | 🚧 | Low-level `call` (no proof coverage). | -| fn | `executeBatch(Call[] calldata)` | 🚧 | Bounded `for` OK. | -| fn | `validateUserOp(PackedUserOperation calldata, bytes32, uint256) returns (uint256)` | ✅ | | -| fn | `_requireFromEntryPoint() internal virtual` | ✅ | | -| fn | `_requireForExecute() internal virtual` | ✅ | | -| fn (internal abstract) | `_validateSignature(PackedUserOperation calldata, bytes32) returns (uint256)` | ❌ | Virtual template requires first-class modifiers/overrides. | -| fn (internal virtual view) | `_validateNonce(uint256)` | ✅ | | -| fn (internal virtual) | `_payPrefund(uint256)` | 🚧 | Low-level `call{value:...}`. | - -## accounts/SimpleAccount.sol - -| Construct | Signature | Verity | Notes | -| --- | --- | --- | --- | -| storage | `address public owner` | ✅ | | -| storage | `IEntryPoint private immutable _entryPoint` | 🚧 | As `address`. | -| event | `SimpleAccountInitialized(IEntryPoint indexed, address indexed)` | ✅ | | -| modifier | `onlyOwner()` | ✅ | Precondition-only user modifiers inline as require prefixes. | -| error | `NotOwner(address,address,address)` | ✅ | | -| error | `NotOwnerOrEntryPoint(address,address,address,address)` | ✅ | | -| inheritance | `is BaseAccount, TokenCallbackHandler, UUPSUpgradeable, Initializable` | ❌ | No multi-inheritance. | -| fn (public view override) | `entryPoint() returns (IEntryPoint)` | 🚧 | | -| receive | `receive() external payable {}` | ❌ | | -| constructor | `constructor(IEntryPoint) { … _disableInitializers() }` | ❌ | Inheritance init. | -| fn (internal view) | `_onlyOwner()` | ✅ | | -| fn (public initializer) | `initialize(address)` | ❌ | `initializer` modifier from OZ. | -| fn (internal virtual) | `_initialize(address)` | ✅ | | -| fn (internal view override) | `_requireForExecute()` | ✅ | | -| fn (internal override virtual) | `_validateSignature(...)` | ❌ | `ECDSA.recover` on typed-data hash → EIP-712 flow (#2002). | -| fn (public view) | `getDeposit() returns (uint256)` | ✅ | | -| fn (public payable) | `addDeposit()` | ✅ | | -| fn (public virtual onlyOwner) | `withdrawDepositTo(address payable, uint256)` | ❌ | Depends on modifier system. | -| fn (internal view override) | `_authorizeUpgrade(address)` | ❌ | UUPS override slot. | - -## core/UserOperationLib.sol - -| Construct | Signature | Verity | Notes | -| --- | --- | --- | --- | -| error | `InvalidPaymasterSignatureLength(uint256,uint256)` | ✅ | | -| const | 6× uint256 offsets (`PAYMASTER_*_OFFSET`, `PAYMASTER_SIG_MAGIC_LEN`, `PAYMASTER_SUFFIX_LEN`) | ✅ | | -| const | `PAYMASTER_SIG_MAGIC = bytes8(0x22e325a297439656)` | ❌ | `bytes8` unsupported. Workaround: `bytes32` masked. | -| const | `MIN_PAYMASTER_DATA_WITH_SUFFIX_LEN` | ✅ | | -| const | `PACKED_USEROP_TYPEHASH = keccak256("...")` | 🚧 | Landed via `keccak256_lit` (#1978). | -| fn | `gasPrice(userOp) view returns (uint256)` | 🚧 | Uses `unpackUints` — packing math OK as `uint256 >> 128`. | -| fn | `encode(userOp, bytes32) pure returns (bytes)` | ❌ | `abi.encode` of 8 args. | -| fn | `unpackUints(bytes32) pure returns (uint256,uint256)` | ✅ | Bitshift form. | -| fn | `unpackHigh128(bytes32) pure returns (uint256)` | ✅ | | -| fn | `unpackLow128(bytes32) pure returns (uint256)` | ❌ | `uint128(uint256(packed))` narrowing. Workaround: `packed & (2**128-1)`. | -| fn | `unpackMaxPriorityFeePerGas(userOp)` | ✅ | | -| fn | `unpackMaxFeePerGas(userOp)` | ❌ | Via `unpackLow128`. | -| fn | `unpackVerificationGasLimit(userOp)` | ✅ | | -| fn | `unpackCallGasLimit(userOp)` | ❌ | Via `unpackLow128`. | -| fn | `unpackPaymasterVerificationGasLimit(userOp)` | ❌ | `bytes16` cast on calldata slice. | -| fn | `unpackPostOpGasLimit(userOp)` | ❌ | Same. | -| fn | `unpackPaymasterStaticFields(bytes calldata)` | ❌ | `bytes20`/`bytes16` slice casts. | -| fn | `getPaymasterSignatureLength(bytes calldata) returns (uint256)` | ❌ | `bytes8`/`bytes2` casts + `uint16`. | -| fn | `getSignedPaymasterData / getPaymasterSignature / getPaymasterSignatureWithLength` | 🚧 | Depend on above. | -| fn | `encodePaymasterSignature(bytes calldata) returns (bytes)` | ❌ | `abi.encodePacked` + `uint16`. | -| fn | `hash(PackedUserOperation calldata, bytes32) returns (bytes32)` | ❌ | Wraps `encode`. | - -## core/EntryPoint.sol — storage + designated entrypoints - -Full file 1013 LOC; storage layout is scored comprehensively, only the -designated entrypoints (`handleOps`, `handleAggregatedOps`, -`_validatePrepayment`, `_executeUserOp`, `depositTo` [inherited], -`getUserOpHash`) are scored at body granularity. (`simulateValidation` lives -in `EntryPointSimulations.sol`, out of the pinned scope.) - -### Storage / constants - -| Construct | Signature | Verity | Notes | -| --- | --- | --- | --- | -| const | `INNER_GAS_OVERHEAD = 10000` (uint256) | ✅ | | -| const | `INNER_OUT_OF_GAS = hex"deaddead"` (bytes32) | ✅ | | -| const | `INNER_REVERT_LOW_PREFUND = hex"deadaa51"` (bytes32) | ✅ | | -| const | `REVERT_REASON_MAX_LEN = 2048` (uint256) | ✅ | | -| const | `UNUSED_GAS_PENALTY_PERCENT = 10` (uint256) | ✅ | | -| const | `PENALTY_GAS_THRESHOLD = 40000` (uint256) | ✅ | | -| const | `VALIDITY_BLOCK_RANGE_FLAG = 0x800000000000` (uint48) | ❌ | `uint48` type. Workaround: `uint256` const. | -| const | `VALIDITY_BLOCK_RANGE_MASK = 0x7fffffffffff` (uint48) | ❌ | Same. | -| storage | `SenderCreator private immutable _senderCreator = new SenderCreator()` | ❌ | `new` (CREATE) inside a state init. | -| const | `DOMAIN_NAME = "ERC4337"` (string) | ❌ | `string` constant. Workaround: `bytes32`. | -| const | `DOMAIN_VERSION = "1"` (string) | ❌ | Same. | -| storage | `bytes32 transient private currentUserOpHash` | ✅ | Transient supported. | -| error | `Reentrancy()` | ✅ | | -| modifier | `nonReentrant()` (`tx.origin == msg.sender && msg.sender.code.length == 0`) | 🚧 | Slot is available (#2076) but custom impl uses `tx.origin`; needs verifier hook. | -| inheritance | `is IEntryPoint, StakeManager, NonceManager, ERC165, EIP712` | ❌ | 5-way inheritance. | -| struct (memory) | `MemoryUserOp` (10 fields) | 🚧 | Memory-only; flatten to multi-return. | -| struct (memory) | `UserOpInfo { MemoryUserOp, bytes32, uint256, uint256, uint256 }` | 🚧 | Nested — flatten. | -| constructor | `constructor() EIP712(DOMAIN_NAME, DOMAIN_VERSION)` | ❌ | Parent ctor + EIP-712 (#2002). | - -### Designated entrypoints (function-body scoring) - -| Fn | Visibility / Mut | Verity | Body notes | -| --- | --- | --- | --- | -| `handleOps(PackedUserOperation[] calldata ops, address payable beneficiary)` | external virtual `nonReentrant` | 🚧 | Signature OK; body uses bounded `for` (✅), `new UserOpInfo[](opslen)` memory alloc (🚧), calls `_iterateValidationPhase`/`_executeUserOp`/`_compensate`. | -| `handleAggregatedOps(UserOpsPerAggregator[] calldata, address payable)` | external virtual `nonReentrant` | ❌ | `try/catch` around `aggregator.validateSignatures` (❌). | -| `_validatePrepayment(uint256, PackedUserOperation calldata, UserOpInfo memory) returns (uint256, uint256)` | internal virtual | 🚧 | Calls `getUserOpHash` (❌ EIP-712) and packs 7 gas values with `|` (✅). `require(maxGasValues <= type(uint120).max, ...)` (`uint120` ❌; workaround `2**120 - 1`). | -| `_executeUserOp(uint256, PackedUserOperation calldata, UserOpInfo memory) returns (uint256)` | internal virtual | ❌ | `abi.encodeCall` (❌), inline assembly with `call`/returndata (🚧), `bytes4` `methodSig` matching (❌). | -| `depositTo(address)` (inherited) | public virtual payable | ✅ | Straight-line. | -| `getUserOpHash(PackedUserOperation calldata) view returns (bytes32)` | public view | ❌ | `MessageHashUtils.toTypedDataHash(getDomainSeparatorV4(), userOp.hash(...))` — full EIP-712 (#2002). | -| `simulateValidation` | — | n/a | In `EntryPointSimulations.sol`, out of pinned scope. | - -### Other EntryPoint functions (signature-level only) - -| Fn | Verity | Blocking construct | -| --- | --- | --- | -| `getCurrentUserOpHash() view returns (bytes32)` | ✅ | Reads transient. | -| `getSenderAddress(bytes calldata)` | ❌ | Calls factory. Depends on CREATE. | -| `senderCreator() view returns (ISenderCreator)` | 🚧 | Return as `address`. | -| `delegateAndRevert(address, bytes calldata)` | ❌ | `.delegatecall` + revert-with-`bytes`. | -| `getPackedUserOpTypeHash() pure returns (bytes32)` | 🚧 | `keccak256_lit` (#1978). | -| `getDomainSeparatorV4() view returns (bytes32)` | ❌ | EIP-712 domain (#2002). | -| `supportsInterface(bytes4) view returns (bool)` | ❌ | `bytes4` arg + `type(...).interfaceId`. | -| `_compensate(address payable, uint256)` | 🚧 | Low-level `.call{value}`. | -| `_emitUserOperationEvent` | ✅ | | -| `_emitPrefundTooLow` | ✅ | | -| `_iterateValidationPhase(...)` | 🚧 | Bounded `for` + internal calls. | -| `innerHandleOp(bytes, UserOpInfo, bytes calldata) returns (uint256)` | ❌ | 63/64 gas math + inline assembly revert + enum `PostOpMode`. | -| `_copyUserOpToMemory` | ❌ | Memory struct writes + `unpackUints` chain. | -| `_getRequiredPrefund` | ✅ | Pure sum × mul. | -| `_createSenderIfNeeded` | ❌ | `senderCreator().createSender{gas:...}(initCode)`. | -| `_validateAccountPrepayment` | 🚧 | Straight-line branching. | -| `_callValidateUserOp` | ❌ | Inline assembly `call` + `returndatasize` + `abi.encodeCall`. | -| `_validatePaymasterPrepayment` | 🚧 | | -| `_callValidatePaymasterUserOp` | ❌ | Heavy inline assembly + `abi.encodeCall`. | -| `_validateAccountAndPaymasterValidationData` | 🚧 | | -| `_getValidationData(uint256) returns (address, bool, bool)` | ❌ | Reads `uint48` `validAfter/validUntil` + masks. | -| `_postExecution` | ❌ | `try/catch` around `IPaymaster(paymaster).postOp{gas:...}` + enum. | -| `_getUserOpGasPrice` | ✅ | | -| `_getOffsetOfMemoryBytes` / `_getMemoryBytesFromOffset` | 🚧 | Pure inline-asm identity. | -| `_getFreePtr` / `_restoreFreePtr` | ❌ | Direct free-pointer manipulation. | -| `_getUnusedGasPenalty` | ✅ | Pure arithmetic. | - -## Cross-reference to open verity tickets - -| Verity ticket | Covers | -| --- | --- | -| [#2071](https://github.com/lfglabs-dev/verity/issues/2071) | Umbrella tracker for this parity work. | -| [#2002](https://github.com/lfglabs-dev/verity/issues/2002) | EIP-712 typed-hash + ecrecover flow. | -| [#2060](https://github.com/lfglabs-dev/verity/issues/2060) | Packed storage lowering. | -| [#1982](https://github.com/lfglabs-dev/verity/issues/1982) | Dynamic ABI codec + storage `bytes`/`string` + typed CodeData. | -| [#1724](https://github.com/lfglabs-dev/verity/issues/1724) | General Solidity-feature parity umbrella. | -| [#1993](https://github.com/lfglabs-dev/verity/issues/1993) | Automatic checked-arithmetic obligations. | -| #1978 *(merged)* | `keccak256_lit` — unblocked `PACKED_USEROP_TYPEHASH`. | -| #2076 *(merged)* | `nonReentrant` lock release — partially unblocks EntryPoint's modifier. | -| #1969 *(merged)* | ERC-4337 frame primitives in EVM/Trace core. | -| #1576 *(merged)* | Receive/fallback macro entrypoints. | - -## Summary - -Total scored constructs: **~175** (11 struct fields · 21 errors · 13 events · -14 storage/constant slots · 4 modifiers · ~88 functions · 6 structs · 1 enum · -5 inheritance headers · 2 constructors · 2 receives · plus 8 designated -EntryPoint bodies scored separately). - -| Bucket | Count | Share | -| --- | --- | --- | -| ✅ expressible today | 75 | 43 % | -| 🚧 expressible with workaround | 42 | 24 % | -| ❌ needs new EDSL work | 58 | 33 % | - -### Top 5 missing EDSL features (weighted by usage) - -| Rank | Feature | Sites hit | Blocked constructs | -| --- | --- | --- | --- | -| 1 | Sub-256 integer types (`uint128`, `uint120`, `uint112`, `uint64`, `uint48`, `uint32`, `uint192`, `uint16`) | 22 | `DepositInfo`, all nonce math, `_validatePrepayment` uint120 guard, `_getValidationData`, `addStake`, `getNonce`, `unpackLow128`. | -| 2 | Full `abi.encode` / `abi.encodePacked` / `abi.encodeCall` / `abi.encodeWithSelector` codec | 9 | `UserOperationLib.encode`, `_callValidateUserOp`, `_callValidatePaymasterUserOp`, `_executeUserOp` inner call. | -| 3 | Inheritance (`is X, Y`) + parent constructors + `virtual`/`override`/first-class modifier system | 8 | `EntryPoint is …`, `SimpleAccount is …`, `BasePaymaster is …`, `onlyOwner`, `initializer`, `_authorizeUpgrade`. | -| 4 | Fixed-width byte types (`bytes4`, `bytes8`, `bytes16`, `bytes20`, `bytes2`) | 7 | Interface-id checks, `unpackPaymaster*`, `methodSig` dispatch, `PAYMASTER_SIG_MAGIC`. | -| 5 | EIP-712 typed-data hash + domain separator (#2002) | 5 | `getUserOpHash`, `_validateSignature`, `getDomainSeparatorV4`, `getPackedUserOpTypeHash` chain. | - -Runner-ups: `try/catch` (3 sites), `receive/fallback` (2 sites), `enum` -(`PostOpMode` propagates to ~6 fn signatures), storage `bytes`/`string`, -`CREATE`/`CREATE2`. - -### Suggested next 3 PRs to unblock 4337 - -1. **Packed sub-256 integer type support and bit-width casts** — Minimum - viable: `uint128`, `uint64`, `uint48`, `uint32`, `uint192`, `uint120` plus - `uint64(x)`, `uint128(uint256(x))`, and `bytes32 → uintN` packing/unpacking - used across `UserOperationLib` and `NonceManager`. -2. **ABI codec surface (`abi.encode`, `abi.encodePacked`, `abi.encodeCall`, - `abi.encodeWithSelector`) with proof coverage** — pinned to the shape used - by `UserOperationLib.encode` and the two `_callValidate*` helpers. -3. **EIP-712 typed-data hash + `ecrecover` (#2002)** — makes `getUserOpHash`, - `getDomainSeparatorV4`, and `SimpleAccount._validateSignature` provable - end-to-end, unblocks the harness's differential test against upstream - Hardhat. - -After those three land, the residual gap is dominated by modifier/inheritance -and the `bytes4` family — good candidates for a second-wave batch, alongside -`try/catch` and `receive/fallback` reuse from #1576. diff --git a/docs/parity/lido.md b/docs/parity/lido.md deleted file mode 100644 index 8544bcba3f..0000000000 --- a/docs/parity/lido.md +++ /dev/null @@ -1,372 +0,0 @@ -# Lido StakingRouter v3 — Verity EDSL parity gap map - -**Status:** draft — best-effort. The Solidity source is NOT in -`lfglabs-dev/lido-srv3-proof-closure` (that repo is a LaTeX report package). -The Solidity mapped here lives in the upstream Lido monorepo `lidofinance/core` -at the exact revision named in the report's `content/05-reproducibility.tex`: - -- Repo: `lidofinance/core` (branch: `develop`, PR - [#1811 — Staking Router V3](https://github.com/lidofinance/core/pull/1811)) -- Commit: `d088bbc2deac9913b68036d73d35c37aa6279b90` -- Files mapped (from the report's own "Source anchor examples" list): - - `contracts/0.8.25/sr/StakingRouter.sol` (~1158 loc) - - `contracts/0.8.25/sr/SRLib.sol` (~932 loc, external library) - - `contracts/0.8.25/sr/SRStorage.sol`, `SRTypes.sol`, `SRUtils.sol`, `ISRBase.sol` - - `contracts/0.8.25/TopUpGateway.sol` (~424 loc) - - `contracts/0.8.9/oracle/AccountingOracle.sol` (~917 loc) - - `contracts/0.4.24/Lido.sol` (~1558 loc, buffer / reserve / `withdrawDepositableEther`) - -**Support matrix legend** (from -[verity #1724](https://github.com/lfglabs-dev/verity/issues/1724)): - -- ✅ expressible today in `verity_contract` -- 🚧 expressible with a workaround (noted) -- ❌ requires a new EDSL feature (feature name noted) - -**Verity type baseline used below.** ✅: `uint256`, `int256`, `uint8`, -`address`, `bytes32`, `bool`, single mapping, `mapping2`, simple events, custom -errors with scalar payloads, `require`, bounded `for`, `if/else`, ternary, -low-level `call`/`staticcall`/`delegatecall` (no proof coverage). ❌: -`uint128/64/32/24/16`, `int128`, `bytes4/20`, `enum`, storage `string`/`bytes`, -top-level `struct` as storage root, `mapping` depth ≥ 3 (no proof), -multiple inheritance, modifier postludes, `abi.encode`/`abi.encodePacked`, -`try/catch`, `while`, `break/continue`, `CREATE`/`CREATE2`, `transfer`/`send`, -`receive`/`fallback`. - -The report itself scopes to six accounting properties (SRV3-P1 … P6). -Constructs outside that scope are still listed so gaps can be counted honestly. - ---- - -## 1. `contracts/0.8.25/sr/SRTypes.sol` - -### 1.1 Enums - -| Construct | Signature | Status | Notes / needed feature | -|---|---|---|---| -| enum `StakingModuleStatus` | `{Active, DepositsPaused, Stopped}` | ✅ | Native uint8-backed enum declarations, members, and checked casts (#2088). | - -### 1.2 Interfaces (referenced, not implemented) - -| Construct | Signature | Status | Notes | -|---|---|---|---| -| `interface ILido` | `getDepositableEther() view returns (uint256)`, `withdrawDepositableEther(uint256,uint256)` | 🚧 | External-call interface. Verity has no `interface` decl but the calls can be modeled with typed low-level `call` — no proof coverage | -| `interface IAccountingOracle` | `getProcessingState() returns (9-tuple)`, `getLastProcessingRefSlot()` | 🚧 | 9-value return tuple — Verity has partial multi-return; no destructuring assignment | - -### 1.3 Structs (used as storage or in mappings) - -| Struct | Fields (widths) | Status | Feature gap | -|---|---|---|---| -| `StakingModuleConfig` (calldata only) | 7× `uint256` | ✅ | Fits Tuple encoding | -| `StakingModule` (legacy, in storage during migration) | `uint24,address,uint16,uint16,uint16,uint8,string,uint64,uint256,uint256,uint16,uint64,uint64,uint8,uint64` | ❌ | `uint24`, `uint16`, `uint64`, `uint8`, storage `string`, storage struct root | -| `ModuleStateConfig` (1 storage slot, packed) | `address,uint16,uint16,uint16,uint16,StakingModuleStatus(enum),uint8` | ❌ | packed `uint16`+`enum`+`uint8` in one slot — needs `uint16`, `enum`, packed-field support | -| `ModuleStateDeposits` (1 slot) | 4× `uint64` | ❌ | `uint64` | -| `ModuleStateAccounting` (1 slot) | 2× `uint64` | ❌ | `uint64` | -| `RouterStateAccounting` (1 slot) | `uint64` | ❌ | `uint64` | -| `ModuleState` (top-level storage struct) | `config, deposits, accounting, string name` | ❌ | storage `string`, nested storage structs | -| `RouterState` (top-level storage struct) | `mapping(uint256=>ModuleState), EnumerableSet.UintSet, RouterStateAccounting, bytes32, uint24 lastModuleId` | ❌ | top-level struct storage root, `uint24`, EnumerableSet library-typed storage | -| `StakingModuleSummary` (memory) | 3× `uint256` | ✅ | Tuple/return | -| `NodeOperatorSummary` (memory) | 8× `uint256` | ✅ | Tuple/return | -| `StakingModuleDigest` (memory return) | 2 uint256 + `StakingModule` + `StakingModuleSummary` | ❌ | nested struct memory returns | -| `NodeOperatorDigest` (memory return) | `uint256, bool, NodeOperatorSummary` | 🚧 | Flatten to tuple | -| `ValidatorsCountsCorrection` (memory) | 4× `uint256` | ✅ | Tuple | -| `ValidatorExitData` (memory) | `uint256, uint256, bytes` | 🚧 | `bytes` ABI-only in Verity; OK if only calldata/memory | - ---- - -## 2. `contracts/0.8.25/sr/ISRBase.sol` (events + errors) - -### 2.1 Events - -| Event | Signature | Status | Notes | -|---|---|---|---| -| `StakingModuleAdded` | `(uint256 indexed, address, string, address)` | 🚧 | indexed dynamic events are supported; `string` in payload is ABI-only | -| `StakingModuleShareLimitSet` | `(uint256 indexed, uint256, uint256, address)` | ✅ | | -| `StakingModuleFeesSet` | `(uint256 indexed, uint256, uint256, address)` | ✅ | | -| `StakingModuleMaxDepositsPerBlockSet` | `(uint256 indexed, uint256, address)` | ✅ | | -| `StakingModuleMinDepositBlockDistanceSet` | `(uint256 indexed, uint256, address)` | ✅ | | -| `StakingModuleStatusSet` | `(uint256 indexed, StakingModuleStatus, address)` | ✅ | Enum event payloads erase to `uint8` in the ABI. | -| `WithdrawalCredentialsSet` | `(bytes32, address)` | ✅ | | -| `StakingRouterETHDeposited` | `(uint256 indexed, uint256)` | ✅ | | -| `DepositableEthReceived` | `(uint256)` | ✅ | | -| `ExitedAndStuckValidatorsCountsUpdateFailed` | `(uint256 indexed, bytes)` | 🚧 | dynamic `bytes` event supported | -| `RewardsMintedReportFailed` | `(uint256 indexed, bytes)` | 🚧 | same | -| `StakingModuleExitedValidatorsIncompleteReporting` | `(uint256 indexed, uint256)` | ✅ | | -| `WithdrawalsCredentialsChangeFailed` | `(uint256 indexed, bytes)` | 🚧 | dynamic `bytes` | -| `StakingModuleExitNotificationFailed` | `(uint256 indexed, uint256 indexed, bytes)` | 🚧 | dynamic `bytes` | - -### 2.2 Errors (custom, scalar payloads unless noted) - -| Error | Payload | Status | -|---|---|---| -| `InvalidAmountGwei`, `NotAuthorized`, `ZeroAddress`, `ZeroArgument`, `ArraysLengthMismatch`, `OracleExtraDataNotSubmitted` | none | ✅ | -| `InvalidReportData` | `(uint256)` | ✅ | -| `ReportedExitedValidatorsExceedDeposited` | `(uint256, uint256)` | ✅ | -| `UnexpectedCurrentValidatorsCount` | `(uint256, uint256)` | ✅ | -| `UnexpectedFinalExitedValidatorsCount` | `(uint256, uint256)` | ✅ | -| `UnrecoverableModuleError`, `ExitedValidatorsCountCannotDecrease`, `DirectETHTransfer`, `ModuleReturnExceedTarget`, `StakingModuleStatusTheSame`, `EmptyKeysList`, `WrongPubkeyLength`, `AmountNotAlignedToGwei`, `AllocationExceedsLimit`, `ZeroDeposits`, `StakingModuleAddressExists`, `StakingModulesLimitExceeded`, `StakingModuleWrongName`, `StakingModuleUnregistered`, `StakingModuleNotActive`, `WrongWithdrawalCredentialsType`, `InvalidPriorityExitShareThreshold`, `InvalidMinDepositBlockDistance`, `InvalidMaxDepositPerBlockValue`, `InvalidStakeShareLimit`, `InvalidFeeSum`, `InconsistentFeeSum` | none | ✅ | -| `UnexpectedModuleId` | `(uint256, uint256)` | ✅ | - ---- - -## 3. `contracts/0.8.25/sr/StakingRouter.sol` - -### 3.1 Top-level construct - -| Construct | Status | Feature gap | -|---|---|---| -| `contract StakingRouter is ISRBase, AccessControlEnumerableUpgradeable` | ❌ | inheritance (`is X, Y`) — no Verity equivalent; must flatten | - -### 3.2 State constants & immutables - -| Var | Type | Status | Notes | -|---|---|---|---| -| 9× `bytes32 public constant *_ROLE = keccak256(...)` | `bytes32` | ✅ | keccak of literal string OK | -| `FEE_PRECISION_POINTS` | `uint256 constant` | ✅ | | -| `PUBKEY_LENGTH` | `uint64 constant` | ❌ | `uint64` — workaround: `uint256` | -| `DEPOSIT_CONTRACT`, `LIDO`, `LIDO_LOCATOR` | `I* immutable` | 🚧 | Verity has immutables for scalars; `interface` type as `address` | -| `MAX_EFFECTIVE_BALANCE_WC_TYPE_01/02` | `uint256 immutable` | ✅ | | - -### 3.3 Constructor & initializers - -| Function | Signature | Status | Notes | -|---|---|---|---| -| `constructor(address,address,address,uint256,uint256)` | — | ✅ | Verity supports constructor + immutable init | -| `initialize(address _admin, bytes32 _wc) external reinitializer(4)` | — | ❌ | `reinitializer(4)` modifier — modifier system missing | -| `finalizeUpgrade_v4() external reinitializer(4)` | — | ❌ | modifier + inline delete of legacy role storage | - -### 3.4 Functions (grouped) - -For brevity: `external onlyRole(X)` in every management function collapses into -"❌ modifier system". - -| Function (signature abbreviated) | Visibility / mut. | Status | Notes / gap | -|---|---|---|---| -| `INITIAL_DEPOSIT_SIZE() view returns (uint256)` | external | ✅ | | -| `TOTAL_BASIS_POINTS() pure returns (uint256)` | external | ✅ | | -| `MAX_STAKING_MODULES_COUNT() pure returns (uint256)` | external | ✅ | | -| `MAX_STAKING_MODULE_NAME_LENGTH() pure returns (uint256)` | external | ✅ | | -| `_getStorageRoleMembersOld() private pure returns (mapping storage $)` | private pure | ❌ | inline assembly + returning `mapping storage` — needs storage-pointer + assembly | -| `addStakingModule(string calldata,address,StakingModuleConfig calldata)` | external, `onlyRole` | 🚧 | body OK once modifier lowered | -| `updateStakingModule(uint256,uint256×6)` | external, `onlyRole` | 🚧 | 7 scalar args | -| `updateAllStakingModulesFees(uint256[] calldata,uint256[] calldata)` | external | 🚧 | calldata arrays OK, bounded for | -| `updateModuleShares(uint256, uint16, uint16)` | external | ❌ | `uint16` params | -| `updateTargetValidatorsLimits(uint256, uint256, uint256, uint256)` | external | 🚧 | forwards to module via `call` | -| `reportRewardsMinted(uint256[], uint256[])` | external | 🚧 | SRV3-P4/P5 core path | -| `updateExitedValidatorsCountByStakingModule(uint256[], uint256[])` | external | 🚧 | | -| `reportValidatorBalancesByStakingModule(uint256[], uint256[])` | external | 🚧 | SRV3-P3 core path; writes `uint64` balance sums — needs `uint64` | -| `validateReportValidatorBalancesByStakingModule(uint256[], uint256[])` | external view | ✅ | pure array validation | -| `reportStakingModuleExitedValidatorsCountByNodeOperator(uint256, bytes, bytes)` | external | 🚧 | `bytes` calldata OK | -| `unsafeSetExitedValidatorsCount(uint256,uint256,bool,ValidatorsCountsCorrection)` | external | 🚧 | struct-by-value calldata (tuple) | -| `onValidatorsCountsByNodeOperatorReportingFinished()` | external | 🚧 | walk `for` over modules | -| `decreaseStakingModuleVettedKeysCountByNodeOperator(uint256, bytes, bytes)` | external | 🚧 | delegates to module (call) | -| `reportValidatorExitDelay(uint256,uint256,uint256,bytes,uint256)` | external | 🚧 | | -| `onValidatorExitTriggered(ValidatorExitData[] calldata,uint256,uint256)` | external | 🚧 | struct array | -| `getStakingModules() view returns (StakingModule[])` | external | ❌ | returns array of legacy struct with narrow ints + `string` | -| `getStakingModuleStateConfig / …StateDeposits / …StateAccounting(uint256)` | external view | ❌ | returns packed narrow-int structs | -| `getStakingModuleIds() view returns (uint256[])` | external | ✅ | | -| `getStakingModule(uint256) view returns (StakingModule)` | external | ❌ | same struct | -| `getStakingModulesCount() view returns (uint256)` | external | ✅ | | -| `hasStakingModule(uint256) view returns (bool)` | public | ✅ | | -| `getStakingModuleStatus(uint256) view returns (StakingModuleStatus)` | public | ✅ | Enum returns erase to `uint8` in the ABI. | -| `getContractVersion() view returns (uint256)` | external | ✅ | | -| `getStakingModuleSummary` / `getNodeOperatorSummary` | external view | ✅ | scalar tuple returns | -| `getAllStakingModuleDigests / getStakingModuleDigests(uint256[])` | external view | ❌ | nested-struct memory arrays | -| `getAllNodeOperatorDigests / getNodeOperatorDigests(...)` | external view | 🚧 | flatten to tuple[] | -| `setStakingModuleStatus(uint256, StakingModuleStatus)` | external `onlyRole` | ✅ | Enum parameters and storage values are supported. | -| `getStakingModuleIsStopped / IsDepositsPaused / IsActive` | external view returns (bool) | ❌ | derived from enum status | -| `getStakingModuleNonce / LastDepositBlock / MinDepositBlockDistance / MaxDepositsPerBlock` | external view returns (uint256) | 🚧 | value stored as `uint64`; cast up | -| `getStakingModuleActiveValidatorsCount(uint256) view returns (uint256, uint256)` | external | ✅ | | -| `getStakingModuleWithdrawalCredentials(uint256) view returns (bytes32)` | external | ✅ | | -| `getStakingModuleMaxDepositsCount(uint256, uint256) view returns (uint256)` | external | ✅ | | -| `canDeposit(uint256) view returns (bool)` | external | ✅ | | -| `receiveDepositableEther() external payable` | external payable | ❌ | payable function needs `receive`/`fallback`/payable modeling (partial) | -| `topUp(uint256, IStakingModuleV2.TopUpsData calldata, bytes calldata)` | external, `onlyRole`, calls out | 🚧 | ABI arrays OK; per-key allocation loop; `abi.encodePacked` on `bytes32 wc` (❌); modifier ❌ | -| `_validateTopUpInputs(uint256, uint256, uint256[] calldata) internal pure` | internal pure | ✅ | | -| `getStakingFeeAggregateDistribution() view returns (uint96 total, uint96 mod, uint96 treas, uint256 prec)` | external | ❌ | `uint96` × 3 | -| `getStakingRewardsDistribution() view returns (address[], uint256[], uint96[], uint96 totalFee, uint256 prec)` | external | ❌ | `uint96` × 2 + `uint96[]` (SRV3-P5 anchor) | -| `getModuleValidatorsBalance(uint256) view returns (uint256)` | external | 🚧 | Wraps `uint64` internal store | -| `getTotalModulesValidatorsBalance() view returns (uint256)` | external | 🚧 | Same | -| `_computeModuleFee(uint256, uint256, uint256, uint16) view returns (uint256)` | internal | ❌ | `uint16` arg | -| `getTotalFeeE4Precision() view returns (uint16)` | external | ❌ | `uint16` return | -| `getStakingFeeAggregateDistributionE4Precision() view returns (uint16, uint16, uint16)` | external | ❌ | `uint16` returns | -| `getDepositAllocations(uint256, bool) view returns (uint256[])` | external | ✅ | | -| `deposit(uint256, bytes calldata) external` | external | 🚧 | SRV3-P2 core path; body uses `address(this).balance` (❌), `abi.encodePacked` (❌), external `call` (🚧); enum comparison ❌ | -| `setWithdrawalCredentials(bytes32) external onlyRole` | external | 🚧 | modifier + notifying loop | -| `getWithdrawalCredentials() public view returns (bytes32)` | public view | ✅ | | -| `_setWithdrawalCredentials(bytes32) internal` | internal | ✅ | | -| `_getWithdrawalCredentialsWithType(uint8) internal view returns (bytes32)` | internal | ✅ | | -| `_updateModuleLastDepositState(uint256, uint256) internal` | internal | 🚧 | writes `uint64 lastDepositBlock` — needs `uint64` | -| `_getModuleDepositAllocation(uint256, uint256, bool) view returns (uint256)` | internal | ✅ | | -| `_getStakingModuleNodeOperatorsCount / …Active…Count(IStakingModule) internal view` | internal | 🚧 | external `staticcall` | -| `_getStakingModuleNodeOperatorIds(IStakingModule, uint256, uint256) view returns (uint256[])` | internal | 🚧 | staticcall | -| `_getStakingModuleNodeOperatorIsActive(IStakingModule, uint256) view returns (bool)` | internal | 🚧 | staticcall | -| `_getModuleState / _getModuleStateCompat / _getStakingModuleSummary / …Struct / _getNodeOperatorSummary` | internal view | 🚧/❌ | Compat variants build legacy struct with narrow ints | -| `_getAccountingOracle / _getTopUpGateway / _getDepositSecurityModule() internal view returns (address)` | internal | ✅ | via locator staticcall | -| `_checkAppAuth(address)` | internal view | ✅ | | -| `_getConfig() private view returns (SRLib.Config)` | private | ✅ | | -| `_toE4Precision(uint256, uint256) pure returns (uint16)` | internal pure | ❌ | `uint16` return | - -### 3.5 Modifiers used but not declared here - -- `onlyRole(bytes32)` (from OZ AccessControl) — used on ~20 external funcs — ❌ modifier system + inheritance -- `reinitializer(4)` — used on `initialize`, `finalizeUpgrade_v4` — ❌ modifier system - ---- - -## 4. `contracts/0.8.25/sr/SRLib.sol` (external library) - -Library used through `using X for Y` — Verity has no library concept; would -inline. - -| Construct | Status | Feature gap | -|---|---|---| -| `library SRLib` | ❌ | `library` decl — must inline | -| `struct Config { uint256, uint256 }` | ✅ | scalar tuple | -| `struct ModuleParamsCache { uint256, uint256, uint16, StakingModuleStatus(enum), uint8 }` | ❌ | `uint16` + enum | -| `_migrateStorage(uint256)` | ❌ | `keccak256` of literal (✅), `delete` on storage slot (❌), `abi.encode`/`abi.encodePacked` (❌), inline assembly slot compute (❌), builds/writes `uint64` fields + `uint24 lastModuleId` (❌), storage `string name` (❌) | -| `_getStorageStakingModulesMapping / _getStorageStakingIndicesMapping` | ❌ | returns `mapping storage` via inline assembly | -| `_addModule(address, string calldata, StakingModuleConfig calldata) returns (uint256)` | ❌ | writes storage `string name`, `uint24 lastModuleId++`, packs `ModuleStateConfig` | -| `_validateShareParams(uint256, uint256) pure` | ✅ | | -| `_updateModuleParams(uint256, uint256×6)` | ❌ | writes `uint16` fields into packed slot | -| `_requireConsistentFeeSum(uint256, uint256, uint256) view` | ✅ | bounded `for` + `require` | -| `_updateAllModuleFees(uint256[] calldata, uint256[] calldata)` | ❌ | writes packed `uint16` fields | -| `_updateModuleShares(uint256, uint256, uint256)` | ❌ | writes packed `uint16` fields | -| `_setModuleStatus(uint256, StakingModuleStatus) returns (bool)` | ❌ | enum arg + writes enum-in-packed-struct | -| `_getStakingModuleSummary(IStakingModule) view returns (uint256, uint256, uint256)` | 🚧 | staticcall (no proof) | -| `_getDepositAllocations(Config calldata, uint256, bool) view returns (uint256, uint256[])` | ✅ | SRV3-P2 anchor — pure array/loop | -| `_getModuleDepositAllocation(uint256, uint256, bool) view returns (uint256)` | ✅ | | -| `_getModulesAllocationAndCapacity(Config calldata, uint256, bool) view returns (uint256[], ModuleParamsCache[])` | ❌ | `ModuleParamsCache[]` contains `uint16` + enum | -| `_reportValidatorExitDelay(uint256, uint256, uint256, bytes calldata, uint256)` | 🚧 | forwarded call | -| `_onValidatorExitTriggered(ValidatorExitData[] calldata, uint256, uint256)` | 🚧 | | -| `_reportRewardsMinted(uint256[] calldata, uint256[] calldata)` | 🚧 | SRV3-P4/P5 anchor: try/catch pattern via low-level `call` + `returndataSize` | -| `_onValidatorsCountsByNodeOperatorReportingFinished()` | 🚧 | loop + call | -| `_decreaseStakingModuleVettedKeysCountByNodeOperator / _reportStakingModuleOperatorExitedValidators / _updateExitedValidatorsCountByStakingModule` | ❌ | Read/write `uint64 exitedValidatorsCount` | -| `_unsafeSetExitedValidatorsCount(...)` | ❌ | Same `uint64` gap | -| `_validateReportValidatorBalancesByStakingModule(uint256[], uint256[]) view` | ✅ | SRV3-P3 anchor | -| `_reportValidatorBalancesByStakingModule(uint256[], uint256[])` | ❌ | SRV3-P3 write path: writes `uint64 validatorsBalanceGwei` | -| `_updateModuleLastDepositState(uint256)` | ❌ | writes `uint64 lastDepositBlock/At` | -| `_notifyStakingModulesOfWithdrawalCredentialsChange()` | 🚧 | loop + low-level `call` | -| `_checkOperatorsReportData(bytes calldata, bytes calldata) internal pure` | 🚧 | slicing `bytes calldata` via assembly `calldataload` | - ---- - -## 5. `contracts/0.8.25/sr/SRStorage.sol` (thin storage-slot helper library) - -| Construct | Status | Feature gap | -|---|---|---| -| `library SRStorage` | ❌ | `library` | -| `bytes32 constant ROUTER_STORAGE_POSITION = keccak256(abi.encode(uint256(keccak256(abi.encodePacked("..."))) - 1)) & ~bytes32(uint256(0xff))` | ❌ | ERC-7201 slot derivation uses `abi.encode` + `abi.encodePacked` | -| `getRouterState() pure returns (RouterState storage $)` | ❌ | inline assembly `$.slot := _position` + top-level struct storage root | -| All `getModuleState`, `addModuleId`, `getModulesCount`, `getModuleIdInnerPosition`, `isModuleExists` | ❌ | OZ `EnumerableSet.UintSet` — 3-deep mapping style; Verity lacks `mappingChain` proof | - ---- - -## 6. `contracts/0.8.25/TopUpGateway.sol` - -| Construct | Status | Feature gap | -|---|---|---| -| `struct Storage { uint64, uint32, uint32, uint16, uint16, uint64, uint64 }` (packed one-slot) | ❌ | `uint64`, `uint32`, `uint16` | -| `bytes32 constant GATEWAY_STORAGE_POSITION` (ERC-7201) | ❌ | `abi.encode/Packed` | -| `uint256 constant PUBKEY_LENGTH = 48` | ✅ | | -| `uint256 constant FAR_FUTURE_EPOCH = type(uint64).max` | ❌ | `type(T).max` unavailable in EDSL | -| `uint256 immutable SLOTS_PER_EPOCH` | ✅ | | -| Roles `TOP_UP_ROLE`, `MANAGE_LIMITS_ROLE` | ✅ | | -| `constructor(uint64, uint256)` | ❌ | `uint64` arg | -| `topUp(TopUpsData calldata, ...)` | 🚧 | modifier `onlyRole` ❌; body reads `bytes32 wc` via external `staticcall`; uses `abi.encodePacked` on pubkey concat (❌) | -| `_verifyValidatorState(...)` | ❌ | EIP-4788 beacon root proof — Merkle/SSZ (out of scope) | -| `_computeTopUpLimit(Validator, uint64) view returns (uint256)` | ❌ | `uint64` arg | -| `_gatewayStorage() pure returns (Storage storage $)` | ❌ | inline asm + packed narrow-int storage struct | -| Events: `MaxValidatorsPerTopUpChanged / MinBlockDistanceChanged / LastTopUpChanged / MaxRootAgeChanged / TopUpBalanceLimitsChanged` | ✅ | | -| Errors `ZeroValue`, `TooLargeValue`, `RootIsTooOld`, `RootPrecedesLastTopUp`, `WrongArrayLength` | ✅ | | -| Error `ZeroArgument(string argument)` | ❌ | dynamic `string` payload | - ---- - -## 7. `contracts/0.8.9/oracle/AccountingOracle.sol` (SRv3 balance-check path only) - -| Construct | Status | Feature gap | -|---|---|---| -| `contract AccountingOracle is BaseOracle` | ✅ | flattened single inheritance; virtual overrides specialize at compile time | -| `initialize(...)` / `finalizeUpgrade_v5(uint256)` | ❌ | modifiers | -| `submitReportData(ReportData calldata, uint256)` | ❌ | large calldata struct with nested fields | -| `_checkStakingRouterModuleBalances(sanityChecker, data, timeElapsed)` (SRV3-P3 P4 anchor) | 🚧 | staticcall + bounded loop | -| `_normalizeStakingRouterValidatorBalancesToWei(uint256[])` | ✅ | pure multiplication | -| `_processStakingRouterExitedValidatorsByModule / …ValidatorBalancesByModule` | 🚧 | forwarded external calls | -| `_processExtraDataItems / _processExtraDataItem(bytes calldata, ExtraDataIterState)` | ❌ | slices `bytes calldata` via assembly `calldataload`, packs `bytes4` selectors, `while`-style | -| `_storageExtraDataProcessingState() pure returns (StorageExtraDataProcessingState storage r)` | ❌ | assembly slot pointer + storage struct | -| Storage struct `StorageExtraDataProcessingState` (packed) | ❌ | contains `uint64` fields | -| Events + errors (~15, mostly scalar) | ✅ | | - ---- - -## 8. `contracts/0.4.24/Lido.sol` (buffer / reserve / withdraw path only) - -Solidity 0.4.24 dialect — Verity targets 0.8.x — the accounting logic would -have to be re-expressed. - -| Construct | Status | Feature gap | -|---|---|---| -| `contract Lido is Versioned, StETHPermit, AragonApp` (multiple inheritance) | ❌ | inheritance (deep) | -| `bytes32 constant BUFFERED_ETHER_POSITION` — packed slot `uint128 depositedPostReport | uint128 bufferedEther` | ❌ | `uint128`, packed 2×`uint128` | -| `bytes32 constant DEPOSITS_RESERVE_POSITION` (single `uint256` slot) | ✅ | ERC-1967 unstructured storage | -| `struct BufferedEtherAllocation { uint256 total, unfinalizedStETH, unreserved, depositsReserve, withdrawalsReserve }` (memory only) | ✅ | scalar tuple | -| `event DepositsReserveSet(uint256)` | ✅ | | -| `event ExternalBadDebtInternalized(uint256)` | ✅ | | -| `_getBufferedEtherAllocation() internal view returns (BufferedEtherAllocation)` (SRV3-P1 anchor) | ✅ | pure `min` arithmetic | -| `getDepositsReserve() external view returns (uint256)` | ✅ | | -| `getDepositableEther() external view returns (uint256)` | ✅ | | -| `withdrawDepositableEther(uint256, uint256) external` (SRV3-P1 anchor) | ❌ | writes `uint128 depositedPostReport` (packed) + 2300-gas `transfer`. Requires `uint128` + `transfer` | -| `receive() external payable` / `fallback()` | ❌ | receive/fallback | -| `_getBufferedEther() internal view returns (uint256)` (extracts low 128 bits) | ❌ | `uint128` + bitmask extraction | -| `_setBufferedEther(uint256) internal` | ❌ | `uint128` packed write | - ---- - -## Summary - -**Total top-level constructs counted:** 176 - -| Category | ✅ | 🚧 | ❌ | Total | -|---|---:|---:|---:|---:| -| Structs / Enums | 5 | 2 | 12 | 19 | -| State variables & constants | 12 | 3 | 8 | 23 | -| Events | 9 | 5 | 1 | 15 | -| Errors | 30 | 0 | 1 | 31 | -| Functions (external + internal + library) | 21 | 27 | 32 | 80 | -| Contract-level (inheritance, receive/fallback, modifiers-used) | 0 | 0 | 8 | 8 | -| **Total** | **77 (43.8%)** | **37 (21.0%)** | **62 (35.2%)** | **176** | - -Only the SRv3-P1…P6 accounting core skews friendlier: ~55 % ✅ / 30 % 🚧 / -15 % ❌ — the ❌ items are concentrated in **`uint64` balance storage** and -**`enum` status gating**. - -### Top 3 missing EDSL features weighted by usage count - -1. **Narrow unsigned integer widths** (`uint64`, `uint16`, `uint32`, `uint24`, - `uint96`, `uint128`) — hit in almost every storage struct and both SRV3 - core writes. **~45 distinct construct hits.** Blocks P3 write path and P1 - write path outright. -2. **`enum` support** — `StakingModuleStatus` in status gating (SRV3-P6), - setters, getters, events, storage packing, args. **~14 construct hits.** - Workaround exists (`Uint8`) but propagates through every signature and the - packed slot. -3. **Modifier system + inheritance** — `onlyRole`, `reinitializer`, `is - ISRBase, AccessControlEnumerableUpgradeable`. **~30 construct hits.** No - in-block workaround today; must flatten. - -Runner-ups: storage `string`, top-level storage structs, `abi.encode` / -`abi.encodePacked`, inline-assembly storage pointer resolution. - -### Verdict on the six P0 properties - -- **SRV3-P1 (reserve separation)** — math ✅; write side (`withdrawDepositableEther`, packed `uint128|uint128` slot) ❌ until `uint128`. -- **SRV3-P2 (exact 32 ETH pull)** — allocation math ✅; `deposit()` body ❌/🚧 (`abi.encodePacked(wc)`, beacon deposit). -- **SRV3-P3 (module-balance conservation)** — validation ✅; accepted-report write writes `uint64 validatorsBalanceGwei`. Blocked on `uint64`. -- **SRV3-P4 (report-before-reward)** — expressible once `uint64` reads exist. -- **SRV3-P5 (bounded rewards / fee alignment)** — invariant ✅; types (`uint96 totalFee`, `uint96[]`) ❌. -- **SRV3-P6 (status gates)** — depends on `enum StakingModuleStatus`; ❌ / 🚧 with `Uint8` shim. - -**Net:** three of six properties can be *stated* today with modest workarounds -(P2, P4, P5 partial). P1, P3, P6 will not close without the top-3 EDSL -features. diff --git a/docs/parity/morpho.md b/docs/parity/morpho.md deleted file mode 100644 index dff397ec28..0000000000 --- a/docs/parity/morpho.md +++ /dev/null @@ -1,260 +0,0 @@ -# Morpho Blue → Verity EDSL parity gap map - -## Preamble - -**Target:** `Th0rgal/morpho-verity@master`, which vendors -`morpho-org/morpho-blue@0427acea` as a git submodule at `./morpho-blue/`. The -project itself is Lean (Yul is only in build artifacts). Solidity sources are -therefore analyzed from the upstream submodule `morpho-org/morpho-blue/src/`. - -**Scope.** Morpho Blue is a single ~550 LOC singleton (`Morpho.sol`) plus five -pure libraries (`MathLib`, `SharesMathLib`, `MarketParamsLib`, `UtilsLib`, -`SafeTransferLib`), a constants file, an events library, and a string-based -errors library. **MetaMorpho is out of scope** — it lives in a separate repo -(`morpho-org/metamorpho`) and `morpho-verity` does not vendor it. The -periphery view-only library `MorphoLib.sol` uses `extSloads` bit-tricks; per -the project's `MORPH_BLUE_MAPPING.md`, the Verity port replaces it with direct -storage projections and is therefore excluded from the parity target. - -**Verity EDSL support model.** -- ✅ expressible today in `verity_contract`. -- 🚧 expressible with a documented workaround. -- ❌ needs a new EDSL feature (named in the row). - -**Key EDSL constraints that dominate Morpho parity:** -1. No `uint128` / `uint64` etc. — Morpho's `Market` and `Position` are 100% - `uint128` for packing. -2. No top-level storage `struct` — Morpho storage is dominated by `mapping(Id - => Market)` and `mapping(Id => mapping(address => Position))`. -3. No mapping-of-mapping-of-mapping with proofs (2 max) — `position[id][user]` - is fine (mapping2), and `MarketParams` inside `idToMarketParams[id]` is a - struct-in-mapping. -4. Single inheritance and precondition-only modifiers are supported; multiple - inheritance, modifier postludes, `abi.encode`, and `try/catch` remain gaps. -5. No callback (`onMorphoSupply`, `onMorphoRepay`, `onMorphoSupplyCollateral`, - `onMorphoLiquidate`, `onMorphoFlashLoan`) trust-boundary primitive. - -Rows marked 🚧 with "packed struct in mapping (#1976)" assume the EDSL packs -the `uint128` fields itself; the developer still writes them as `uint128`, -which is ❌ standalone. Where both apply, the deeper blocker is surfaced. - -Total constructs counted: **72**. - ---- - -## 1. `src/Morpho.sol` (555 LOC, singleton) - -### 1.1 Immutables & storage - -| # | Construct | Solidity | Verity status | Note | -|---|-----------|----------|---------------|------| -| 1 | Immutable | `bytes32 public immutable DOMAIN_SEPARATOR` | 🚧 | `bytes32` ✅ but immutable not first-class; store in constructor-initialized slot. | -| 2 | Storage | `address public owner` | ✅ | | -| 3 | Storage | `address public feeRecipient` | ✅ | | -| 4 | Storage | `mapping(Id => mapping(address => Position)) public position` | ❌ | Storage `struct Position` + `uint128` fields → **packed struct in mapping (#1976)** + `mapping2` ✅ + `uint128` ❌. Blocker: `uint128`. | -| 5 | Storage | `mapping(Id => Market) public market` | ❌ | `Market` = 6× `uint128` packed into 2 slots. Needs **`uint128` + packed struct in mapping (#1976)**. | -| 6 | Storage | `mapping(address => bool) public isIrmEnabled` | ✅ | | -| 7 | Storage | `mapping(uint256 => bool) public isLltvEnabled` | ✅ | | -| 8 | Storage | `mapping(address => mapping(address => bool)) public isAuthorized` | ✅ | | -| 9 | Storage | `mapping(address => uint256) public nonce` | ✅ | | -| 10 | Storage | `mapping(Id => MarketParams) public idToMarketParams` | 🚧 | `Id` is `bytes32` ✅. `MarketParams` = 4×`address` + `uint256` — no packing needed. **struct-in-mapping (#1976)**. | - -### 1.2 Types (from `IMorpho.sol`, used pervasively) - -| # | Construct | Solidity | Verity status | Note | -|---|-----------|----------|---------------|------| -| 11 | User type | `type Id is bytes32;` | 🚧 | `bytes32` ✅; the newtype wrapper is sugar the frontend can inline. | -| 12 | Struct | `MarketParams { address loanToken, collateralToken, oracle, irm; uint256 lltv; }` | 🚧 | All fields ✅; memory struct usage needs **local-struct** or manual field passing. | -| 13 | Struct | `Position { uint256 supplyShares; uint128 borrowShares; uint128 collateral; }` | ❌ | `uint128` fields → **`uint128`** required. | -| 14 | Struct | `Market { uint128 totalSupplyAssets, totalSupplyShares, totalBorrowAssets, totalBorrowShares, lastUpdate, fee; }` | ❌ | 100% `uint128` → **`uint128`** required. | -| 15 | Struct | `Authorization { address authorizer, authorized; bool isAuthorized; uint256 nonce, deadline; }` | 🚧 | Memory only; all fields ✅ if flattened. | -| 16 | Struct | `Signature { uint8 v; bytes32 r, s; }` | ✅ | | - -### 1.3 Constructor & modifier - -| # | Construct | Solidity | Verity status | Note | -|---|-----------|----------|---------------|------| -| 17 | Constructor | `constructor(address newOwner)` | ✅ | `keccak256(abi.encode(...))` inside → 🚧 (see row 20). | -| 18 | Modifier | `modifier onlyOwner()` | ✅ | Precondition-only modifiers are inlined as require prefixes. | -| 19 | Inheritance | `contract Morpho is IMorphoStaticTyping` | ✅ | Flattened single inheritance is supported. | -| 20 | Call | `keccak256(abi.encode(DOMAIN_TYPEHASH, block.chainid, address(this)))` | ❌ | **`abi.encode`** not supported. | - -### 1.4 Owner functions - -| # | Function | Solidity | Verity status | Note | -|---|----------|----------|---------------|------| -| 21 | `setOwner(address) external onlyOwner` | ✅ | Inline modifier. | -| 22 | `enableIrm(address) external onlyOwner` | ✅ | | -| 23 | `enableLltv(uint256) external onlyOwner` | ✅ | | -| 24 | `setFee(MarketParams, uint256) external onlyOwner` | 🚧 | Blocked only by `market[id].fee = uint128(newFee)` cast → **`uint128`**. | -| 25 | `setFeeRecipient(address) external onlyOwner` | ✅ | | - -### 1.5 Market creation - -| # | Function | Solidity | Verity status | Note | -|---|----------|----------|---------------|------| -| 26 | `createMarket(MarketParams) external` | ❌ | `market[id].lastUpdate = uint128(block.timestamp)` and `idToMarketParams[id] = marketParams` need **`uint128`** and **struct-in-mapping**; `IIrm(...).borrowRate(...)` is low-level ✅ once storage blockers land. | - -### 1.6 Core entrypoints - -| # | Function | Solidity | Verity status | Note | -|---|----------|----------|---------------|------| -| 27 | `supply(MarketParams, uint256, uint256, address, bytes) external returns (uint256, uint256)` | ❌ | Reads/writes `market[id].totalSupplyAssets/Shares` (`uint128`), + callback `IMorphoSupplyCallback(msg.sender).onMorphoSupply` — **flashloan/reentrant callback pattern** not in EDSL. | -| 28 | `withdraw(MarketParams, uint256, uint256, address, address) external returns (uint256, uint256)` | ❌ | Same `uint128` field arithmetic. | -| 29 | `borrow(MarketParams, uint256, uint256, address, address) external returns (uint256, uint256)` | ❌ | `uint128` arithmetic; `_isHealthy` call ✅ shape-wise. | -| 30 | `repay(MarketParams, uint256, uint256, address, bytes) external returns (uint256, uint256)` | ❌ | Same `uint128` blockers + `onMorphoRepay` callback ❌. | -| 31 | `supplyCollateral(MarketParams, uint256, address, bytes) external` | ❌ | `position[id][onBehalf].collateral += assets.toUint128()`; `onMorphoSupplyCollateral` callback ❌. | -| 32 | `withdrawCollateral(MarketParams, uint256, address, address) external` | ❌ | `uint128` cast on `collateral`. | -| 33 | `liquidate(MarketParams, address, uint256, uint256, bytes) external returns (uint256, uint256)` | ❌ | Heaviest: `uint128` arithmetic on both `market[id]` and `position[id][borrower]`, `onMorphoLiquidate` callback ❌, oracle `IOracle(...).price()` ✅, math (`wMulDown`/`wDivUp`/`mulDivUp`/`mulDivDown`) ✅. | -| 34 | `flashLoan(address, uint256, bytes) external` | ❌ | Pure **flashloan callback pattern** — `onMorphoFlashLoan` sandwiched between `safeTransfer` and `safeTransferFrom`. | - -### 1.7 Authorization - -| # | Function | Solidity | Verity status | Note | -|---|----------|----------|---------------|------| -| 35 | `setAuthorization(address, bool) external` | ✅ | `mapping2` write + event. | -| 36 | `setAuthorizationWithSig(Authorization, Signature) external` | ❌ | `abi.encode(AUTHORIZATION_TYPEHASH, authorization)` ❌ + `bytes.concat("\x19\x01", ...)` ❌ + `ecrecover` (not on ✅ list). | -| 37 | `_isSenderAuthorized(address) internal view returns (bool)` | ✅ | Pure boolean. | - -### 1.8 Interest accrual & health - -| # | Function | Solidity | Verity status | Note | -|---|----------|----------|---------------|------| -| 38 | `accrueInterest(MarketParams) external` | 🚧 | Requires `market[id].lastUpdate` (`uint128`). | -| 39 | `_accrueInterest(MarketParams, Id) internal` | ❌ | Reads/writes 5 `uint128` fields; `wTaylorCompounded`, `wMulDown` ✅; `IIrm.borrowRate` ✅. | -| 40 | `_isHealthy(MarketParams, Id, address) internal view returns (bool)` | ❌ | Reads `position[id][borrower].borrowShares` (`uint128`). | -| 41 | `_isHealthy(MarketParams, Id, address, uint256) internal view returns (bool)` | ❌ | `uint256(position[id][borrower].borrowShares).toAssetsUp(...)` needs `uint128` field read. Math ✅. | - -### 1.9 Storage view - -| # | Function | Solidity | Verity status | Note | -|---|----------|----------|---------------|------| -| 42 | `extSloads(bytes32[]) external view returns (bytes32[])` | ❌ | Inline `assembly { sload }` over a calldata array. Verity has no **raw `sload` opcode / dynamic bytes32 return**. Workaround: expose typed getters. | - ---- - -## 2. Libraries - -### 2.1 `MathLib.sol` - -| # | Function | Verity | Note | -|---|----------|--------|------| -| 43 | `wMulDown(uint256, uint256)` | ✅ | Native proven op. | -| 44 | `wDivDown(uint256, uint256)` | ✅ | Implied by `mulDivDown(x, WAD, y)`. | -| 45 | `wDivUp(uint256, uint256)` | ✅ | Native proven op. | -| 46 | `mulDivDown(uint256, uint256, uint256)` | ✅ | Native proven op. | -| 47 | `mulDivUp(uint256, uint256, uint256)` | ✅ | Native proven op. | -| 48 | `wTaylorCompounded(uint256, uint256)` | ✅ | Composable; Taylor is manual. | - -### 2.2 `SharesMathLib.sol` - -| # | Function | Verity | Note | -|---|----------|--------|------| -| 49 | `toSharesDown` | ✅ | Pure `mulDivDown`. | -| 50 | `toAssetsDown` | ✅ | Pure `mulDivDown`. | -| 51 | `toSharesUp` | ✅ | Pure `mulDivUp`. | -| 52 | `toAssetsUp` | ✅ | Pure `mulDivUp`. | -| 53 | Constant `VIRTUAL_SHARES = 1e6` | ✅ | | -| 54 | Constant `VIRTUAL_ASSETS = 1` | ✅ | | - -### 2.3 `MarketParamsLib.sol` - -| # | Function | Verity | Note | -|---|----------|--------|------| -| 55 | `id(MarketParams memory) → Id` via `assembly { keccak256(marketParams, 160) }` | 🚧 | Result is `keccak256` of 5 contiguous 32-byte words. EDSL can express as `keccak256(loanToken, collateralToken, oracle, irm, lltv)` if a **fixed-arity keccak of ABI-encoded words** helper is added; else `abi.encode` ❌. | - -### 2.4 `UtilsLib.sol` - -| # | Function | Verity | Note | -|---|----------|--------|------| -| 56 | `exactlyOneZero(uint256, uint256)` | ✅ | | -| 57 | `min(uint256, uint256)` | ✅ | Ternary. | -| 58 | `toUint128(uint256)` | ❌ | Returns **`uint128`**. | -| 59 | `zeroFloorSub(uint256, uint256)` | ✅ | | - -### 2.5 `SafeTransferLib.sol` - -| # | Function | Verity | Note | -|---|----------|--------|------| -| 60 | `safeTransfer(IERC20, address, uint256)` | 🚧 | Uses `abi.encodeCall` ❌ + returndata decode ❌; Verity port replaces with an **ECM primitive** (see `MORPH_BLUE_MAPPING.md`). | -| 61 | `safeTransferFrom(IERC20, address, address, uint256)` | 🚧 | Same ECM pattern. | - -### 2.6 `ConstantsLib.sol` - -| # | Constant | Verity | Note | -|---|----------|--------|------| -| 62 | `MAX_FEE = 0.25e18` | ✅ | | -| 63 | `ORACLE_PRICE_SCALE = 1e36` | ✅ | | -| 64 | `LIQUIDATION_CURSOR = 0.3e18` | ✅ | | -| 65 | `MAX_LIQUIDATION_INCENTIVE_FACTOR = 1.15e18` | ✅ | | -| 66 | `DOMAIN_TYPEHASH = keccak256("EIP712Domain(...)")` | ✅ | Compile-time. | -| 67 | `AUTHORIZATION_TYPEHASH = keccak256("Authorization(...)")` | ✅ | Compile-time. | - -### 2.7 `ErrorsLib.sol` - -All 21 error identifiers are `string internal constant` used as `require` -reason strings. - -| # | Construct | Verity | Note | -|---|-----------|--------|------| -| 68 | 21× `string internal constant NAME` | 🚧 | Storage `string` ❌, but these are **library constants**, not storage — inline as literal reason strings at each `require`. Or convert to custom errors (✅). | - -### 2.8 `EventsLib.sol` — 18 events - -| # | Event | Verity | Note | -|---|-------|--------|------| -| 69 | `SetOwner`, `SetFee`, `SetFeeRecipient`, `EnableIrm`, `EnableLltv`, `Supply`, `Withdraw`, `Borrow`, `Repay`, `SupplyCollateral`, `WithdrawCollateral`, `FlashLoan`, `SetAuthorization`, `IncrementNonce`, `AccrueInterest` | ✅ | Fields all `address`/`uint256`/`bool`/`Id (bytes32)` — events with proofs supported. | -| 70 | `CreateMarket(Id indexed id, MarketParams marketParams)` | 🚧 | Non-indexed **struct payload** in event. Emit as 5 flat fields until struct-in-event is supported. | -| 71 | `Liquidate(Id, address, address, 5×uint256)` | ✅ | Flat scalar payload. | - -### 2.9 Interface files (`IMorpho.sol`, `IIrm.sol`, `IOracle.sol`, `IMorphoCallbacks.sol`, `IERC20.sol`) - -| # | Construct | Verity | Note | -|---|-----------|--------|------| -| 72 | External interfaces used via low-level `call`/`staticcall` (`IIrm.borrowRate`, `IOracle.price`, `IERC20.transfer/transferFrom`, 5× callback interfaces) | ✅ / ❌ | Static/low-level calls ✅ for IRM+Oracle+ERC20; **the 5 callback interfaces** need the **flashloan/callback trust-boundary** ❌. | - ---- - -## 3. Summary - -**Counted constructs: 72** - -| Bucket | Count | % | -|--------|-------|---| -| ✅ expressible today | 34 | 47% | -| 🚧 workaround exists | 13 | 18% | -| ❌ needs new EDSL feature | 25 | 35% | - -### Top 5 missing EDSL features (weighted by usage in Morpho) - -| Rank | Feature | Directly blocks | Why it dominates | -|------|---------|-----------------|------------------| -| 1 | **`uint128` scalar type** | rows 4, 5, 13, 14, 24, 26, 27–33, 38–41, 58 (~15 constructs) | Every write to `Market` or `Position` casts to `uint128`; nothing accrual/health/liquidation-related compiles without it. | -| 2 | **Packed struct storage in `mapping` (#1976 completion for 6×`uint128`)** | rows 4, 5, 10, 26 | `Market` packs 6 `uint128`s into 2 slots; `Position` packs `uint128+uint128` into 1 slot; #1976 support needs to cover 6-field packing to match Solidity's layout used by `extSloads`. | -| 3 | **Callback / re-entrant trust boundary (`onMorpho*`)** | rows 27, 30, 31, 33, 34, 72 | 5 of 8 core entrypoints run user-supplied code mid-execution. `flashLoan` is nothing but this pattern. | -| 4 | **`abi.encode` / `abi.encodePacked` / `bytes.concat`** | rows 17, 20, 36, 55 | EIP-712 paths and the market-id keccak use ABI encoding. Alternative: "keccak of typed tuple" primitive. | -| 5 | **`ecrecover` precompile** | row 36 | `setAuthorizationWithSig` needs it; unlocks any future meta-tx feature. | - -### Recommended "next 3 PRs to unblock Morpho" - -1. **PR-A: `uint128` scalar + packed-mapping storage (Market & Position - layouts).** Land `uint128` arithmetic + widen #1976 packed-struct support - to 6 fields with the exact slot layout Solidity uses. Immediately unblocks - `createMarket`, `_accrueInterest`, `supply`, `withdraw`, `borrow`, `repay`, - `supplyCollateral`, `withdrawCollateral`, `_isHealthy` — reduces ❌ from - 25 → ~9. -2. **PR-B: Callback trust-boundary primitive (`onMorpho*` pattern).** A - `verity_contract` construct that models "emit event → external `call` to - caller → resume with post-state assertion" as an ECM trust boundary — - reuses the existing ECM pattern already used by `SafeTransferLib` in the - Verity port. Unblocks `flashLoan`, `supply`, `repay`, `supplyCollateral`, - `liquidate`. -3. **PR-C: EIP-712 helpers — `abi.encode`-of-tuple + `ecrecover`.** Even a - narrow `keccak256_of_words(bytes32, ...)` plus `ecrecover(bytes32, uint8, - bytes32, bytes32)` closes `setAuthorizationWithSig`, the constructor's - `DOMAIN_SEPARATOR`, and `MarketParamsLib.id`. - -After these three PRs, `>90%` of Morpho Blue's Solidity would map -line-by-line into `verity_contract`. Remaining ❌ items (inheritance, -first-class modifiers, `extSloads` raw `sload` array) are cosmetic or -replaceable with typed getters and do not block correctness parity. diff --git a/docs/roadmap/log.md b/docs/roadmap/log.md deleted file mode 100644 index 62fedf76d8..0000000000 --- a/docs/roadmap/log.md +++ /dev/null @@ -1,27 +0,0 @@ -# Roadmap log - -Chronological record of merges, closes, and cross-track decisions driven by the -roadmap agent. One line per event, newest last. - -Format: `YYYY-MM-DD | | | ` - -Tracks: -- **T1** — Fix CI -- **T2** — Prevent #2074/#2075-class regressions -- **T3** — Extend the proven fragment (issue #1723) -- **T4** — Refactor proofs into a lemma library -- **T5** — Prune the issue tracker -- **T6** — Faithful Solidity representation of Lido / ERC-4337 / Morpho - -## Log - -- 2026-07-02 | PR #2078 | T1 | Hardened `issue-intake-guard.yml` against - script injection via `findings` output (env-var read instead of inline - string interpolation). -- 2026-07-02 | PR (this) | T6 | Seeded `docs/parity/{lido,erc4337,morpho}.md` - gap maps: Lido 44% ✅, ERC-4337 43% ✅, Morpho 47% ✅. Convergent top-3 - missing EDSL features across all three contracts: narrow uints - (`uint128`/`uint64`/`uint48`), `abi.encode*` codec, first-class modifiers + - inheritance. -- 2026-07-02 | PR (this) | T5 | Created `docs/roadmap/log.md` reporting - infrastructure. diff --git a/examples/external-libs/README.md b/examples/external-libs/README.md index 2b999e0d5e..0c279daf80 100644 --- a/examples/external-libs/README.md +++ b/examples/external-libs/README.md @@ -190,7 +190,7 @@ External libraries are **outside the formal verification boundary**. Your Lean p 1. Use audited, battle-tested library implementations 2. Add Foundry tests that exercise linked contracts end-to-end -3. Document the trust assumption (see [TRUST_ASSUMPTIONS.md](../../TRUST_ASSUMPTIONS.md#5-external-library-code-linker)) +3. Document the trust assumption (see [TRUST_ASSUMPTIONS.md](../../docs/TRUST_ASSUMPTIONS.md#5-external-library-code-linker)) ## Related Files diff --git a/lakefile.lean b/lakefile.lean index cb41bf7d54..260cd8e7ee 100644 --- a/lakefile.lean +++ b/lakefile.lean @@ -22,75 +22,6 @@ lean_lib «Verity» where .one `Verity.Proofs.LoopSimulationResultAware ] -input_file vaultSolidity where - path := "Contracts/VaultFromSolidity/Vault.sol" - text := false - -input_file vaultLeanImporter where - path := "Contracts/VaultFromSolidity/Importer/Importer.lean" - text := false - -input_file vaultLeanSyntax where - path := "Contracts/VaultFromSolidity/Importer/Syntax.lean" - text := false - -input_file vaultLeanSemantics where - path := "Contracts/VaultFromSolidity/Importer/Semantics.lean" - text := false - -input_file vaultSolc where - path := ".lake/solidity-import/solc" - text := false - -input_file vaultBuildPolicy where - path := "lakefile.lean" - text := false - -lean_lib «VaultSolidityImporter» where - globs := #[.one `Contracts.VaultFromSolidity.Importer.Syntax, - .one `Contracts.VaultFromSolidity.Importer.Semantics, - .one `Contracts.VaultFromSolidity.Importer.Importer] - -lean_lib «VaultFromSolidity» where - globs := #[.one `Contracts.VaultFromSolidity.VaultFromSolidity, - .one `Contracts.VaultFromSolidity.Spec, - .one `Contracts.VaultFromSolidity.Proofs.ExecutionProof] - needs := #[vaultSolidity, vaultLeanImporter, vaultLeanSyntax, vaultLeanSemantics, vaultSolc, - vaultBuildPolicy] - -input_file inheritanceSolidity where - path := "Contracts/SolidityImportSmoke/Inheritance/Inheritance.sol" - text := false - -lean_lib «SolidityImportSmokeInheritance» where - globs := #[.one `Contracts.SolidityImportSmoke.Inheritance.Inheritance, - .one `Contracts.SolidityImportSmoke.Inheritance.Spec, - .one `Contracts.SolidityImportSmoke.Inheritance.Proofs] - needs := #[inheritanceSolidity, vaultLeanImporter, vaultLeanSyntax, vaultLeanSemantics, vaultSolc, - vaultBuildPolicy] - -input_file modifiersSolidity where - path := "Contracts/SolidityImportSmoke/Modifiers/Modifiers.sol" - text := false - -lean_lib «SolidityImportSmokeModifiers» where - globs := #[.one `Contracts.SolidityImportSmoke.Modifiers.Modifiers, - .one `Contracts.SolidityImportSmoke.Modifiers.Spec, - .one `Contracts.SolidityImportSmoke.Modifiers.Proofs] - needs := #[modifiersSolidity, vaultLeanImporter, vaultLeanSyntax, vaultLeanSemantics, vaultSolc, - vaultBuildPolicy] - -input_file structsSolidity where - path := "Contracts/SolidityImportSmoke/Structs/Structs.sol" - text := false - -lean_lib «SolidityImportSmokeStructs» where - globs := #[.one `Contracts.SolidityImportSmoke.Structs.Structs, - .one `Contracts.SolidityImportSmoke.Structs.Spec, - .one `Contracts.SolidityImportSmoke.Structs.Proofs] - needs := #[structsSolidity, vaultLeanImporter, vaultLeanSyntax, vaultLeanSemantics, vaultSolc, - vaultBuildPolicy] - lean_lib «Contracts» where globs := #[ .one `Contracts, @@ -120,6 +51,16 @@ lean_lib «Contracts» where .andSubmodules `Contracts.ReentrancyRelyGuarantee ] +/-- Solidity read by `solidity_import`: editing it rebuilds the import. -/ +input_dir solidityImportSmokeSources where + path := "Contracts/SolidityImportSmoke" + filter := .extension "sol" + text := true + +lean_lib «SolidityImportSmoke» where + globs := #[.one `Contracts.SolidityImportSmoke.Smoke] + needs := #[solidityImportSmokeSources] + lean_lib «Compiler» where globs := #[.andSubmodules `Compiler] diff --git a/packages/verity-compiler/lakefile.lean b/packages/verity-compiler/lakefile.lean index 0eb65f9daf..e9b3f5d31a 100644 --- a/packages/verity-compiler/lakefile.lean +++ b/packages/verity-compiler/lakefile.lean @@ -21,7 +21,3 @@ lean_lib «Compiler» where lean_exe «verity-compiler» where srcDir := "../.." root := `Compiler.Main - -lean_exe «verity-compiler-patched» where - srcDir := "../.." - root := `Compiler.MainPatched diff --git a/packages/verity-examples/lakefile.lean b/packages/verity-examples/lakefile.lean index 9b3d44e8e9..c97bb9ba61 100644 --- a/packages/verity-examples/lakefile.lean +++ b/packages/verity-examples/lakefile.lean @@ -14,8 +14,6 @@ lean_lib «Contracts» where .one `Contracts.Common, .one `Contracts.Specs, .one `Contracts.Interpreter, - .one `Contracts.MacroTranslateInvariantTest, - .one `Contracts.MacroTranslateRoundTripFuzz, .one `Contracts.Smoke, .andSubmodules `Contracts.Counter, .andSubmodules `Contracts.SimpleStorage, diff --git a/scripts/README.md b/scripts/README.md index 1f41a256ad..e23610d7f9 100644 --- a/scripts/README.md +++ b/scripts/README.md @@ -1,6 +1,6 @@ # Scripts Quickstart -Use this file for day-to-day operation. Detailed script inventory lives in [REFERENCE.md](REFERENCE.md). +Use this file for day-to-day operation. Each script's docstring describes what it checks. ## High-signal commands diff --git a/scripts/REFERENCE.md b/scripts/REFERENCE.md deleted file mode 100644 index b82142aa83..0000000000 --- a/scripts/REFERENCE.md +++ /dev/null @@ -1,73 +0,0 @@ -# Scripts Reference - -This document is the long-form reference for script responsibilities. - -## Verify workflow sync - -- `check_verify_sync.py`: unified table-driven validator for workflow invariants. -- `verify_sync_spec_source.py`: authoritative verify workflow sync contract. -- `generate_verify_sync_spec.py`: regenerates/checks the committed JSON artifact from the Python source. -- `verify_sync_spec.json`: generated mirror of the verify workflow contract for easy inspection and diff review. -- `ci_timeout_watchdog.py`: warns when recent verify jobs spend too much of their timeout budget. -- `check_docs_workflow_sync.py`: keep the docs workflow self-triggering and aligned across push/pull_request path filters. - -## Issue #1060 automation - -- `check_issue_1060_integrity.py`: ledger schema + anti-inflation + repository-fact checks (run in CI). -- `check_issue_templates.py`: validate GitHub issue form structure and fail on accidental CI-log contamination. - -## Artifacts and documentation consistency - -- `docsync.py`: schema-driven doc-sync engine (P7 consolidation). One declarative registry of artifact/doc bindings; run `python3 scripts/docsync.py --check [--only ]` or `--list`. Migrated entries: `low_level_call_boundary`, `linear_memory_boundary`, `axiomatized_primitive_boundary`, `struct_mapping_surface`, `layer2_boundary`, `layer2_boundary_catalog`, `interpreter_feature_boundary_catalog`. The legacy `check_*_sync.py` paths below remain as thin shims. -- `generate_verification_status.py`: refresh/check `artifacts/verification_status.json`. -- `generate_trust_surface_report.py`: refresh/check `artifacts/trust_surface_report.json` (machine-readable trust-surface inventory: mechanisms, ECM assumptions with locations, Lean 4.31 string-fact boundary). -- `generate_layer2_boundary_catalog.py`: refresh/check `artifacts/layer2_boundary_catalog.json`. -- `check_feature_ownership.py`: validate `artifacts/feature_ownership.json`, the major feature-surface ownership and proof-boundary registry. -- `check_verification_status_doc.py`: keep `docs/VERIFICATION_STATUS.md` aligned with the artifact-backed live totals. -- `check_layer2_boundary_sync.py`: shim for `docsync.py --only layer2_boundary` (Layer 2 proof-boundary claims across README/trust/docs/docs-site surfaces). -- `check_layer2_boundary_catalog_sync.py`: shim for `docsync.py --only layer2_boundary_catalog` (Layer 2 docs aligned with the machine-readable boundary catalog). -- `verification_metrics.py`: shared metric collection and strict artifact validation. -- `refresh_verification_artifacts.sh`: regenerate and validate the verification artifact. - -## Property and proof boundaries - -Primary guards: -- `property_pipeline.py`: consolidated property manifest/coverage pipeline (P7 Cluster C). Subcommands: `check [--only manifest|coverage|lean-sync]` (single manifest parse for all three checks), `extract` (regenerate `test/property_manifest.json`), `report` (coverage statistics; `--format`, `--fail-below`). The five legacy scripts (`check_property_manifest.py`, `check_property_coverage.py`, `check_property_manifest_sync.py`, `extract_property_manifest.py`, `report_property_coverage.py`) remain as thin shims. -- `lean_lint.py`: consolidated Lean structure/hygiene lint runner (P7 Cluster E). Dispatcher over rule modules with `--only/--skip/--list`; rules: `contract_structure`, `paths`, `compilationmodel_split`, `axioms`, `trust_surface_registry`, `storage_layout`, `lean_hygiene`, `split_compiler_test_artifacts`, `rewrite_proof_metadata`, `proof_length`, `spec_named_storage`. Each rule module (the legacy `check_*.py` file) keeps its logic and stays directly runnable for arg-bearing CI invocations. -- `check_axioms.py`: validate AXIOMS.md registry locations and parse `PrintAxioms.lean` dependency output (lean_lint rule `axioms`). -- `check_paths.py`: detect case-insensitive checkout hazards and enforce universal Layer-2 bridge quantification (lean_lint rule `paths`). -- `check_property_manifest.py`: shim for `property_pipeline.py check --only manifest`. -- `check_property_coverage.py`: shim for `property_pipeline.py check --only coverage`. -- `check_property_manifest_sync.py`: shim for `property_pipeline.py check --only lean-sync`. -- `check_builtin_bridge_matrix_sync.py`: keep the builtin bridge matrix artifact and docs in sync, including delegated env builtins. -- `check_interpreter_feature_boundary_catalog_sync.py`: shim for `docsync.py --only interpreter_feature_boundary_catalog`. -- `check_interpreter_feature_summary_sync.py`: keep the interpreter feature summary table aligned with the machine-readable feature matrix artifact. -- `check_low_level_call_boundary_sync.py`: shim for `docsync.py --only low_level_call_boundary`. -- `check_linear_memory_boundary_sync.py`: shim for `docsync.py --only linear_memory_boundary`. -- `check_axiomatized_primitive_boundary_sync.py`: shim for `docsync.py --only axiomatized_primitive_boundary`. -- `check_struct_mapping_surface_sync.py`: shim for `docsync.py --only struct_mapping_surface`. -- `check_storage_layout.py` (lean_lint rule `storage_layout`) -- `generate_storage_layout_report.py`: emit the per-contract storage layout JSON artifact (`artifacts/storage_layout_report.json`) and human-readable summary (`artifacts/STORAGE_LAYOUT_SUMMARY.md`) for migration/audit review (#1897). The Lean executable `verity-storage-layout-report` is the JSON source of truth; `--check --no-lean` is the drift gate run by `make check`. -- `check_lean_hygiene.py` (lean_lint rule `lean_hygiene`; the sorry/native_decide gate) -- `check_proof_length.py` (lean_lint rule `proof_length`) -- `check_spec_named_storage.py` (lean_lint rule `spec_named_storage`): opted-in human-facing specs (starting with `Contracts/VaultFromSolidity/Spec.lean`) must use named storage views: no raw `ContractState` accessor (list read from `Verity/Core.lean`), storage field, direct `ContractState` mention, positional projection, or `knownAddresses`. -- `check_macro_health.py` -- `check_compiler_boundaries.py` -- `test_check_struct_mapping_surface_sync.py`: unit coverage for the struct-mapping doc sync guard. - -## Foundry/gas/selector pipeline - -- `check_selectors.py` -- `check_yul.py` -- `check_gas.py` -- `check_patch_gas_delta.py` - -## Helpers and generators - -- `official_solc.py`: committed official solc SHA-256 pins and `list.json` helpers. -- `setup_solc_importer.py`: fetch official `list.json`, verify the published digest, install `.lake/solidity-import/solc`. -- `check_solc_pin.py --verify-published-checksums` / `make check-solc-published`: live list.json pin check. -- `workflow_jobs.py`: shared workflow parsing and command matching helpers. -- `generate_print_axioms.py` -- `generate_contract.py` -- `generate_macro_property_tests.py` diff --git a/scripts/check_axiomatized_primitive_boundary_sync.py b/scripts/check_axiomatized_primitive_boundary_sync.py deleted file mode 100644 index bdd4351640..0000000000 --- a/scripts/check_axiomatized_primitive_boundary_sync.py +++ /dev/null @@ -1,27 +0,0 @@ -#!/usr/bin/env python3 -"""Deprecated shim: this checker now lives in scripts/docsync.py (entry `axiomatized_primitive_boundary`). - -Prefer `python3 scripts/docsync.py --check --only axiomatized_primitive_boundary`. -This wrapper is kept so existing callers of this path keep working. -""" - -from __future__ import annotations - -import sys -from pathlib import Path - -SCRIPT_DIR = Path(__file__).resolve().parent -if str(SCRIPT_DIR) not in sys.path: - sys.path.insert(0, str(SCRIPT_DIR)) - -from docsync import run_entry - -ENTRY = "axiomatized_primitive_boundary" - - -def main() -> int: - return run_entry(ENTRY) - - -if __name__ == "__main__": - raise SystemExit(main()) diff --git a/scripts/check_axioms.py b/scripts/check_axioms.py index 09a34a5cdc..e89607f8cd 100644 --- a/scripts/check_axioms.py +++ b/scripts/check_axioms.py @@ -104,9 +104,9 @@ def discover_repo_axioms() -> dict[str, tuple[str, int]]: def run_location_check() -> int: - axioms_md = ROOT / "AXIOMS.md" + axioms_md = ROOT / "docs" / "AXIOMS.md" if not axioms_md.exists(): - die("AXIOMS.md not found") + die("docs/AXIOMS.md not found") text = axioms_md.read_text(encoding="utf-8") axiom_blocks = parse_axiom_entries(text) diff --git a/scripts/check_bridge_coverage_sync.py b/scripts/check_bridge_coverage_sync.py index 01feef8f0e..5132363546 100644 --- a/scripts/check_bridge_coverage_sync.py +++ b/scripts/check_bridge_coverage_sync.py @@ -10,8 +10,8 @@ ROOT = Path(__file__).resolve().parents[1] BRIDGE_LEMMAS = ROOT / "Compiler" / "Proofs" / "YulGeneration" / "Backends" / "EvmYulLeanBridgeLemmas.lean" TARGET_FILES = { - "TRUST_ASSUMPTIONS": ROOT / "TRUST_ASSUMPTIONS.md", - "AXIOMS": ROOT / "AXIOMS.md", + "TRUST_ASSUMPTIONS": ROOT / "docs" / "TRUST_ASSUMPTIONS.md", + "AXIOMS": ROOT / "docs" / "AXIOMS.md", "ARITHMETIC_PROFILE": ROOT / "docs" / "ARITHMETIC_PROFILE.md", "INTERPRETER_FEATURE_MATRIX": ROOT / "docs" / "INTERPRETER_FEATURE_MATRIX.md", "END_TO_END": ROOT / "Compiler" / "Proofs" / "EndToEnd.lean", diff --git a/scripts/check_contract_structure.py b/scripts/check_contract_structure.py index 2020ff195b..9a063a23a0 100755 --- a/scripts/check_contract_structure.py +++ b/scripts/check_contract_structure.py @@ -21,8 +21,6 @@ "ReentrancyRelyGuarantee", # Proof-only rely-guarantee framework example, inline proofs "Ownable", # Mixin facet: named-slot proofs + footprint, no Foundry/Yul twin "OwnedCounterComposed", # Include-host acceptance example; OwnedCounter keeps Yul/difftest - "VaultFromSolidity", # Proof-only imported model with its own focused structure - "SolidityImportSmoke", # Nested proof-only importer smokes (Inheritance, Modifiers, Structs) } # Contracts excluded from property test check @@ -32,8 +30,6 @@ "ReentrancyRelyGuarantee", # Abstract state-transformer proofs, no compiled contract to property-test "Ownable", # Mixin proofs are reused by hosts; no compiled property harness "OwnedCounterComposed", # Proof-composition host; OwnedCounter remains the Foundry target - "VaultFromSolidity", # Imported-model theorems are covered by the focused mutation suite - "SolidityImportSmoke", # Importer smoke theorems are covered by focused Python suites } # Contracts excluded from differential test check @@ -44,8 +40,6 @@ "ReentrancyRelyGuarantee", # No compiled bytecode (abstract proofs), nothing to differential-test "Ownable", # Mixin facet; no dedicated Yul/Foundry twin "OwnedCounterComposed", # Selectors/layout stay on OwnedCounter until bit-identical - "VaultFromSolidity", # Proof-only importer emits no bytecode for differential testing - "SolidityImportSmoke", # Proof-only importer smoke emits no bytecode } # Expected files for each contract (relative to ROOT) diff --git a/scripts/check_feature_ownership.py b/scripts/check_feature_ownership.py index 8b9be8f13a..f1c02b9c68 100755 --- a/scripts/check_feature_ownership.py +++ b/scripts/check_feature_ownership.py @@ -70,7 +70,6 @@ def main() -> None: "typed_external_interface_calls", "low_level_call_returndata", "foreach_positive_nonempty", - "legacy_spec_aliases", "patched_compiler_entrypoint", "external_call_with_return_ecm_name", } diff --git a/scripts/check_interpreter_feature_boundary_catalog_sync.py b/scripts/check_interpreter_feature_boundary_catalog_sync.py deleted file mode 100644 index 76665dcf4a..0000000000 --- a/scripts/check_interpreter_feature_boundary_catalog_sync.py +++ /dev/null @@ -1,27 +0,0 @@ -#!/usr/bin/env python3 -"""Deprecated shim: this checker now lives in scripts/docsync.py (entry `interpreter_feature_boundary_catalog`). - -Prefer `python3 scripts/docsync.py --check --only interpreter_feature_boundary_catalog`. -This wrapper is kept so existing callers of this path keep working. -""" - -from __future__ import annotations - -import sys -from pathlib import Path - -SCRIPT_DIR = Path(__file__).resolve().parent -if str(SCRIPT_DIR) not in sys.path: - sys.path.insert(0, str(SCRIPT_DIR)) - -from docsync import run_entry - -ENTRY = "interpreter_feature_boundary_catalog" - - -def main() -> int: - return run_entry(ENTRY) - - -if __name__ == "__main__": - raise SystemExit(main()) diff --git a/scripts/check_layer2_boundary_sync.py b/scripts/check_layer2_boundary_sync.py deleted file mode 100644 index fd404b0082..0000000000 --- a/scripts/check_layer2_boundary_sync.py +++ /dev/null @@ -1,27 +0,0 @@ -#!/usr/bin/env python3 -"""Deprecated shim: this checker now lives in scripts/docsync.py (entry `layer2_boundary`). - -Prefer `python3 scripts/docsync.py --check --only layer2_boundary`. -This wrapper is kept so existing callers of this path keep working. -""" - -from __future__ import annotations - -import sys -from pathlib import Path - -SCRIPT_DIR = Path(__file__).resolve().parent -if str(SCRIPT_DIR) not in sys.path: - sys.path.insert(0, str(SCRIPT_DIR)) - -from docsync import run_entry - -ENTRY = "layer2_boundary" - - -def main() -> int: - return run_entry(ENTRY) - - -if __name__ == "__main__": - raise SystemExit(main()) diff --git a/scripts/check_linear_memory_boundary_sync.py b/scripts/check_linear_memory_boundary_sync.py deleted file mode 100644 index 2b02f522d5..0000000000 --- a/scripts/check_linear_memory_boundary_sync.py +++ /dev/null @@ -1,27 +0,0 @@ -#!/usr/bin/env python3 -"""Deprecated shim: this checker now lives in scripts/docsync.py (entry `linear_memory_boundary`). - -Prefer `python3 scripts/docsync.py --check --only linear_memory_boundary`. -This wrapper is kept so existing callers of this path keep working. -""" - -from __future__ import annotations - -import sys -from pathlib import Path - -SCRIPT_DIR = Path(__file__).resolve().parent -if str(SCRIPT_DIR) not in sys.path: - sys.path.insert(0, str(SCRIPT_DIR)) - -from docsync import run_entry - -ENTRY = "linear_memory_boundary" - - -def main() -> int: - return run_entry(ENTRY) - - -if __name__ == "__main__": - raise SystemExit(main()) diff --git a/scripts/check_low_level_call_boundary_sync.py b/scripts/check_low_level_call_boundary_sync.py deleted file mode 100644 index 6cbd90014d..0000000000 --- a/scripts/check_low_level_call_boundary_sync.py +++ /dev/null @@ -1,27 +0,0 @@ -#!/usr/bin/env python3 -"""Deprecated shim: this checker now lives in scripts/docsync.py (entry `low_level_call_boundary`). - -Prefer `python3 scripts/docsync.py --check --only low_level_call_boundary`. -This wrapper is kept so existing callers of this path keep working. -""" - -from __future__ import annotations - -import sys -from pathlib import Path - -SCRIPT_DIR = Path(__file__).resolve().parent -if str(SCRIPT_DIR) not in sys.path: - sys.path.insert(0, str(SCRIPT_DIR)) - -from docsync import run_entry - -ENTRY = "low_level_call_boundary" - - -def main() -> int: - return run_entry(ENTRY) - - -if __name__ == "__main__": - raise SystemExit(main()) diff --git a/scripts/check_mapping_slot_boundary.py b/scripts/check_mapping_slot_boundary.py index ca900c35e0..109c3a5600 100755 --- a/scripts/check_mapping_slot_boundary.py +++ b/scripts/check_mapping_slot_boundary.py @@ -16,7 +16,7 @@ ROOT = Path(__file__).resolve().parents[1] PROOFS_DIR = ROOT / "Compiler" / "Proofs" MAPPING_SLOT_FILE = PROOFS_DIR / "MappingSlot.lean" -TRUST_ASSUMPTIONS_FILE = ROOT / "TRUST_ASSUMPTIONS.md" +TRUST_ASSUMPTIONS_FILE = ROOT / "docs" / "TRUST_ASSUMPTIONS.md" ALLOWED_MAPPING_ENCODING_IMPORTERS: set[Path] = set() diff --git a/scripts/check_parity_pack_metrics.py b/scripts/check_parity_pack_metrics.py deleted file mode 100644 index 26929693da..0000000000 --- a/scripts/check_parity_pack_metrics.py +++ /dev/null @@ -1,104 +0,0 @@ -#!/usr/bin/env python3 -"""Validate parity-pack identity metrics from a generated diff report.""" - -from __future__ import annotations - -import argparse -import json -from pathlib import Path -from typing import Dict - - -ONLY_IN_VERITY_KINDS = { - "file_missing_in_solc", - "scope_missing_in_solc", - "missing_in_solc", -} -ONLY_IN_SOLIDITY_KINDS = { - "file_missing_in_verity", - "scope_missing_in_verity", - "missing_in_verity", -} -HASH_MISMATCH_KINDS = {"line_mismatch"} - - -def parse_args() -> argparse.Namespace: - parser = argparse.ArgumentParser(description=__doc__) - parser.add_argument("--report", required=True, help="Path to yul identity diff report JSON") - parser.add_argument("--max-only-in-verity", type=int, default=0) - parser.add_argument("--max-only-in-solidity", type=int, default=0) - parser.add_argument("--max-hash-mismatch", type=int, default=0) - parser.add_argument("--format", choices=["text", "markdown"], default="text") - return parser.parse_args() - - -def load_report(path: Path) -> dict: - try: - return json.loads(path.read_text(encoding="utf-8")) - except FileNotFoundError as exc: - raise SystemExit(f"Report not found: {path}") from exc - except json.JSONDecodeError as exc: - raise SystemExit(f"Invalid JSON in report {path}: {exc}") from exc - - -def metric_counts(report: dict) -> Dict[str, int]: - by_kind = report.get("summary", {}).get("byKind", {}) - if not isinstance(by_kind, dict): - raise SystemExit("Invalid report: summary.byKind must be an object") - - def sum_kinds(kinds: set[str]) -> int: - return sum(int(by_kind.get(kind, 0)) for kind in kinds) - - return { - "onlyInVerity": sum_kinds(ONLY_IN_VERITY_KINDS), - "onlyInSolidity": sum_kinds(ONLY_IN_SOLIDITY_KINDS), - "hashMismatch": sum_kinds(HASH_MISMATCH_KINDS), - } - - -def print_metrics(metrics: Dict[str, int], fmt: str) -> None: - if fmt == "markdown": - print("### Parity Pack Metrics") - print("| Metric | Count |") - print("| :-- | --: |") - print(f"| `onlyInVerity` | {metrics['onlyInVerity']} |") - print(f"| `onlyInSolidity` | {metrics['onlyInSolidity']} |") - print(f"| `hashMismatch` | {metrics['hashMismatch']} |") - else: - print( - "Parity pack metrics: " - f"onlyInVerity={metrics['onlyInVerity']} " - f"onlyInSolidity={metrics['onlyInSolidity']} " - f"hashMismatch={metrics['hashMismatch']}" - ) - - -def main() -> int: - args = parse_args() - report = load_report(Path(args.report)) - metrics = metric_counts(report) - print_metrics(metrics, args.format) - - violations = [] - if metrics["onlyInVerity"] > args.max_only_in_verity: - violations.append( - f"onlyInVerity={metrics['onlyInVerity']} exceeds max {args.max_only_in_verity}" - ) - if metrics["onlyInSolidity"] > args.max_only_in_solidity: - violations.append( - f"onlyInSolidity={metrics['onlyInSolidity']} exceeds max {args.max_only_in_solidity}" - ) - if metrics["hashMismatch"] > args.max_hash_mismatch: - violations.append( - f"hashMismatch={metrics['hashMismatch']} exceeds max {args.max_hash_mismatch}" - ) - - if violations: - for line in violations: - print(f"ERROR: {line}") - return 1 - return 0 - - -if __name__ == "__main__": - raise SystemExit(main()) diff --git a/scripts/check_solc_pin.py b/scripts/check_solc_pin.py index 59f731657e..48a9940777 100644 --- a/scripts/check_solc_pin.py +++ b/scripts/check_solc_pin.py @@ -25,8 +25,7 @@ VERIFY_YML = ROOT / ".github" / "workflows" / "verify.yml" SETUP_SOLC_ACTION = ROOT / ".github" / "actions" / "setup-solc" / "action.yml" FOUNDRY_TOML = ROOT / "foundry.toml" -TRUST_ASSUMPTIONS = ROOT / "TRUST_ASSUMPTIONS.md" -MACOS_SOLC_SHA256 = official_solc.OFFICIAL_SOLC_SHA256["macosx-amd64"] +TRUST_ASSUMPTIONS = ROOT / "docs" / "TRUST_ASSUMPTIONS.md" SOLC_VERSION_RE = re.compile(r'^\s*SOLC_VERSION:\s*"([^"]+)"\s*$', re.MULTILINE) SOLC_URL_RE = re.compile(r'^\s*SOLC_URL:\s*"([^"]+)"\s*$', re.MULTILINE) @@ -129,11 +128,11 @@ def main(argv: list[str] | None = None) -> int: trust_pin = TRUST_PIN_RE.search(trust_text) if trust_pin is None: errors.append( - "TRUST_ASSUMPTIONS.md: missing pinned solc version line ('**Version**: (pinned)')" + "docs/TRUST_ASSUMPTIONS.md: missing pinned solc version line ('**Version**: (pinned)')" ) elif url_commit is not None and trust_pin.group(1) != f"{solc_version}+commit.{url_commit}": errors.append( - "TRUST_ASSUMPTIONS.md: pinned solc version must match verify.yml SOLC_VERSION/SOLC_URL" + "docs/TRUST_ASSUMPTIONS.md: pinned solc version must match verify.yml SOLC_VERSION/SOLC_URL" ) if SOLC_DOWNLOAD_RE.search(action_text) is None: @@ -145,41 +144,12 @@ def main(argv: list[str] | None = None) -> int: if re.search(r"\bsudo\b", action_text): errors.append(".github/actions/setup-solc/action.yml: solc install step must not require sudo") - importer = ROOT / "Contracts" / "VaultFromSolidity" / "Importer" / "Importer.lean" if linux_sha256 != official_solc.OFFICIAL_SOLC_SHA256["linux-amd64"]: errors.append( ".github/workflows/verify.yml: SOLC_SHA256 must be the official " "linux-amd64 list.json digest" ) - if importer.exists(): - importer_text = _read(importer) - if official_solc.SOLC_LONG_VERSION not in importer_text: - errors.append( - "Contracts/VaultFromSolidity/Importer/Importer.lean: " - f"must pin solcVersionPin {official_solc.SOLC_LONG_VERSION}" - ) - if linux_sha256 not in importer_text: - errors.append( - "Contracts/VaultFromSolidity/Importer/Importer.lean: " - "must pin verify.yml SOLC_SHA256 for linux-amd64" - ) - if MACOS_SOLC_SHA256 not in importer_text: - errors.append( - "Contracts/VaultFromSolidity/Importer/Importer.lean: " - "must pin the official macosx-amd64 solc SHA-256" - ) - if "officialSolcSha256s" not in importer_text: - errors.append( - "Contracts/VaultFromSolidity/Importer/Importer.lean: " - "must accept official solc builds via officialSolcSha256s" - ) - if "/usr/bin/shasum" not in importer_text: - errors.append( - "Contracts/VaultFromSolidity/Importer/Importer.lean: " - "macOS checksum path must be /usr/bin/shasum" - ) - if args.verify_published_checksums: _verify_published_checksums(errors) diff --git a/scripts/check_solidity_differential.sh b/scripts/check_solidity_differential.sh new file mode 100755 index 0000000000..5223c551bd --- /dev/null +++ b/scripts/check_solidity_differential.sh @@ -0,0 +1,11 @@ +#!/bin/sh +set -eu +# Keep the caller's Lake workspace: this script also runs from downstream repos. +verity_test_root=$(CDPATH= cd -- "$(dirname -- "$0")/.." && pwd) +verity_test_venv=".lake/slice-testing-venv" +if [ ! -x "$verity_test_venv/bin/python" ]; then + python3 -m venv "$verity_test_venv" +fi +"$verity_test_venv/bin/python" -m pip install --disable-pip-version-check -q -r "$verity_test_root/scripts/solidity_differential/requirements.txt" +python3 "$verity_test_root/scripts/setup_solc_import.py" --output .lake/solidity-import/solc-0.8.34 +exec "$verity_test_venv/bin/python" "$verity_test_root/scripts/solidity_import_differential.py" "$@" diff --git a/scripts/check_spec_named_storage.py b/scripts/check_spec_named_storage.py deleted file mode 100644 index 0595ba4323..0000000000 --- a/scripts/check_spec_named_storage.py +++ /dev/null @@ -1,133 +0,0 @@ -#!/usr/bin/env python3 -"""Named-storage gate for human-facing specs. - -A spec should say `v.totalAssets`, not `s.readSlot 0`: a wrong slot number -silently points the promise at another variable, and the reader has to trust a -comment to know what the number means. Opted-in spec files therefore must not -touch `ContractState` directly at all: no raw accessor (`readSlot`, `readMap`, -`readMapUint`, `readTransient`, ...), no raw storage field, no `ContractState` -mention, no positional projection, and no Verity ghost `knownAddresses` -bookkeeping. Names come from a generated storage view (see -`Contracts/VaultFromSolidity/Importer/Importer.lean`); `Storage` is a -kernel-checked structure whose `view` reads through `Slot` handles, so -the gate rejects the accessor names wherever they appear rather than trying -to spot a numeric slot argument after them. - -The raw accessor list is read from `Verity/Core.lean` (every definition in the -`ContractState` namespace plus the storage-backing fields), so a new accessor -is covered without editing this file. - -The gate is opt-in per file: handwritten contracts still state specs over raw -slots and are not listed yet. - -Usage: - python3 scripts/check_spec_named_storage.py -""" - -from __future__ import annotations - -import re -import sys -from pathlib import Path - -from property_utils import ROOT, scrub_lean_code - -SPEC_FILES = ( - "Contracts/VaultFromSolidity/Spec.lean", - "Contracts/SolidityImportSmoke/Inheritance/Spec.lean", - "Contracts/SolidityImportSmoke/Modifiers/Spec.lean", - "Contracts/SolidityImportSmoke/Structs/Spec.lean", -) - -CORE_LEAN = "Verity/Core.lean" - -# Fields of `ContractState` that back storage or its ghost bookkeeping. -STORAGE_FIELDS = ("storageWords", "storageArray", "knownAddresses") - -# Ways to take a `Storage`/`ContractState` value apart without naming a field. -STRUCTURE_ESCAPES = ("mk", "rec", "recOn", "casesOn", "noConfusion") - -_DEF_RE = re.compile( - r"^(?:@\[[^\]]*\]\s*)?(?:private\s+|protected\s+|noncomputable\s+)*" - r"(?:def|abbrev|theorem|instance)\s+([A-Za-z_][\w'?!]*)", - re.M, -) - - -def raw_accessors(core_text: str) -> frozenset[str]: - """Every name defined inside `namespace ContractState` in `core_text`.""" - names: set[str] = set() - depth = 0 - for line in scrub_lean_code(core_text).splitlines(): - stripped = line.strip() - if stripped == "namespace ContractState": - depth += 1 - continue - if stripped == "end ContractState": - depth = max(depth - 1, 0) - continue - if depth and (match := _DEF_RE.match(line)): - names.add(match.group(1)) - return frozenset(names) - - -RAW_ACCESSORS = raw_accessors((ROOT / CORE_LEAN).read_text(encoding="utf-8")) - - -def _identifier_re(names: tuple[str, ...] | frozenset[str]) -> re.Pattern[str]: - alternatives = "|".join(re.escape(name) for name in sorted(names)) - return re.compile(r"(? list[tuple[int, str]]: - """Return `(line, message)` for every raw storage reference in Lean `text`.""" - violations: list[tuple[int, str]] = [] - for line_no, line in enumerate(scrub_lean_code(text).splitlines(), 1): - for match in ACCESSOR_RE.finditer(line): - violations.append((line_no, f"raw `ContractState` accessor `{match.group(0)}`")) - for match in FIELD_RE.finditer(line): - if match.group(0) == "knownAddresses": - violations.append((line_no, "`knownAddresses` ghost bookkeeping")) - else: - violations.append((line_no, f"raw storage field `{match.group(0)}`")) - if CONTRACT_STATE_RE.search(line): - violations.append((line_no, "`ContractState` named directly")) - for match in STRUCTURE_ESCAPE_RE.finditer(line): - violations.append((line_no, f"structure eliminator `{match.group(0)}`")) - for match in PROJECTION_RE.finditer(line): - violations.append((line_no, f"positional projection `{match.group(0)}`")) - return violations - - -def main() -> int: - errors: list[str] = [] - if not RAW_ACCESSORS: - errors.append(f"{CORE_LEAN}: found no definitions in `namespace ContractState`") - for rel in SPEC_FILES: - path = ROOT / rel - if not path.is_file(): - errors.append(f"{rel}: opted-in spec file is missing") - continue - for line_no, message in find_violations(path.read_text(encoding="utf-8")): - errors.append(f"{rel}:{line_no}: {message}; use the named storage view instead") - if errors: - print("Spec named-storage check failed:", file=sys.stderr) - for error in errors: - print(f" - {error}", file=sys.stderr) - return 1 - print(f"Spec named-storage check passed ({len(SPEC_FILES)} opted-in spec files).") - return 0 - - -if __name__ == "__main__": - sys.exit(main()) diff --git a/scripts/check_storage_layout.py b/scripts/check_storage_layout.py index 0f0f657964..7655946414 100644 --- a/scripts/check_storage_layout.py +++ b/scripts/check_storage_layout.py @@ -1,15 +1,13 @@ #!/usr/bin/env python3 -"""Validate storage layout consistency across EDSL, Spec, and Compiler layers. +"""Validate storage layout consistency between the EDSL and Spec layers. Extracts storage slot definitions from: -1. EDSL layer: Contracts/*/.lean (StorageSlot definitions) -2. Spec layer: Contracts/*/Spec.lean (StorageSlot definitions) -3. Compiler layer: Compiler/Specs.lean (CompilationModel field lists) +1. EDSL layer: Contracts/*/.lean (StorageSlot or macro storage definitions) +2. Spec layer: Contracts/*/Spec.lean (literal storage slot accesses) Checks: -- No intra-contract slot collisions within any layer -- Cross-layer consistency: EDSL slots match Compiler slot assignments -- Spec layer duplications match EDSL definitions +- No intra-contract slot collisions within either layer +- Spec slot/type usage matches the EDSL for every contract with both layers """ from __future__ import annotations @@ -26,30 +24,10 @@ r"def\s+(\w+)\s*:\s*StorageSlot\s+(.+?)\s*:=\s*⟨(\d+)⟩" ) -# Regex for Compiler fields list entries: -# { name := "", ty := FieldType. } -# { name := "", ty := FieldType.mappingTyped (.simple .address) } -# { name := "", ty := FieldType.mappingTyped (.nested .address .address) } -COMPILER_FIELD_RE = re.compile( - r'\{\s*name\s*:=\s*"(\w+)"\s*,\s*ty\s*:=\s*FieldType\.([\w.() ]+?)\s*\}' -) -COMPILER_ALIAS_RE = re.compile( - r"def\s+(\w+)\s*:\s*CompilationModel\s*:=\s*Contracts\.(\w+)\.spec" -) -COMPILER_FILTERED_ALIAS_RE = re.compile( - r"def\s+(\w+)\s*:\s*CompilationModel\s*:=\s*" - r"let\s+canonical\s*:=\s*Contracts\.(\w+)\.spec\s*" - r"\{\s*canonical\s+with\s+functions\s*:=\s*canonical\.functions\.filter\s+fun\s+fn\s*=>\s*(.*?)\s*\}", - re.DOTALL, -) MACRO_STORAGE_LINE_RE = re.compile( r"^\s*(\w+)\s*:\s*(.+?)\s*:=\s*slot\s+(\d+)\s*$", re.MULTILINE ) -# Regex for CompilationModel name: -# name := "" -SPEC_NAME_RE = re.compile(r'name\s*:=\s*"(\w+)"') - # Regex for Lean namespace: # namespace NAMESPACE_RE = re.compile(r"^namespace\s+(\S+)", re.MULTILINE) @@ -75,32 +53,6 @@ ) -def find_matching(text: str, start: int, open_ch: str, close_ch: str) -> int: - """Return index of matching close delimiter starting at `start`.""" - depth = 0 - for idx in range(start, len(text)): - ch = text[idx] - if ch == open_ch: - depth += 1 - elif ch == close_ch: - depth -= 1 - if depth == 0: - return idx - return -1 - - -def iter_braced_blocks(text: str, header_pattern: re.Pattern[str]) -> list[str]: - """Return braced block bodies following header matches in source text.""" - blocks: list[str] = [] - for match in header_pattern.finditer(text): - body_start = match.end() - 1 # points at opening "{" - body_end = find_matching(text, body_start, "{", "}") - if body_end == -1: - continue - blocks.append(text[body_start + 1 : body_end]) - return blocks - - def extract_macro_contract_slots(contract_name: str) -> list[tuple[str, str, int]]: """Extract storage field declarations from a `Contracts//.lean` file.""" path = ROOT / "Contracts" / contract_name / f"{contract_name}.lean" @@ -161,95 +113,6 @@ def extract_edsl_slots(filepath: Path) -> dict[str, list[tuple[str, str, int]]]: return {} -def extract_compiler_specs(filepath: Path) -> dict[str, list[tuple[str, str, int]]]: - """Extract CompilationModel field definitions from Compiler/Specs.lean. - - Fields get automatic slot assignment based on list order (index-based). - Returns dict mapping contract name to list of (name, type, slot_number). - """ - content = strip_lean_comments(filepath.read_text()) - specs: dict[str, list[tuple[str, str, int]]] = {} - - spec_header_pattern = re.compile( - r"def\s+\w+\s*:\s*CompilationModel\s*:=\s*\{" - ) - for block in iter_braced_blocks(content, spec_header_pattern): - - # Extract contract name - name_match = SPEC_NAME_RE.search(block) - if not name_match: - continue - contract_name = name_match.group(1) - - # Extract fields in order - fields = [] - for idx, field_match in enumerate(COMPILER_FIELD_RE.finditer(block)): - # Only take fields before the first function definition - field_pos = field_match.start() - # Check if we've passed the fields section - functions_marker = block.find("functions :=") - constructor_marker = block.find("constructor :=") - # Fields come before constructor and functions - if functions_marker != -1 and field_pos > functions_marker: - continue - if constructor_marker != -1 and field_pos > constructor_marker: - continue - - field_name = field_match.group(1) - field_type = field_match.group(2) - fields.append((field_name, field_type, idx)) - - if fields: - specs[contract_name] = fields - - for _def_name, contract_name in COMPILER_ALIAS_RE.findall(content): - if contract_name in specs: - continue - fields = extract_macro_contract_slots(contract_name) - if fields: - specs[contract_name] = fields - - for _def_name, contract_name, _filter_body in COMPILER_FILTERED_ALIAS_RE.findall(content): - if contract_name in specs: - continue - fields = extract_macro_contract_slots(contract_name) - if fields: - specs[contract_name] = fields - - return specs - - -def extract_compiler_externals(filepath: Path) -> dict[str, bool]: - """Extract whether each CompilationModel has non-empty externals.""" - content = strip_lean_comments(filepath.read_text()) - externals: dict[str, bool] = {} - - spec_header_pattern = re.compile( - r"def\s+\w+\s*:\s*CompilationModel\s*:=\s*\{" - ) - for block in iter_braced_blocks(content, spec_header_pattern): - name_match = SPEC_NAME_RE.search(block) - if not name_match: - continue - contract_name = name_match.group(1) - - ext_match = re.search(r"externals\s*:=\s*\[", block) - if not ext_match: - externals[contract_name] = False - continue - - list_start = ext_match.end() - 1 - list_end = find_matching(block, list_start, "[", "]") - if list_end == -1: - externals[contract_name] = False - continue - - ext_body = block[list_start + 1 : list_end].strip() - externals[contract_name] = bool(ext_body) - - return externals - - def extract_spec_slots( filepath: Path, ) -> tuple[list[tuple[str, str, int]], list[str]]: @@ -340,93 +203,19 @@ def check_intra_collisions( return errors -def check_layer_consistency( - reference: dict[str, list[tuple[str, str, int]]], - subject: dict[str, list[tuple[str, str, int]]], - label: str, - ref_label: str = "EDSL", - subj_label: str = "", - check_reverse: bool = False, -) -> list[str]: - """Check that subject layer fields match reference layer definitions. - - Args: - reference: The authoritative layer (e.g. EDSL). - subject: The layer to validate against reference. - label: Prefix for error messages (e.g. "Cross-layer", "Spec-EDSL"). - ref_label: Display name for the reference layer. - subj_label: Display name for the subject layer. - check_reverse: If True, also report reference fields missing from subject. - """ - errors = [] - - for contract, subj_fields in subject.items(): - if contract not in reference: - if check_reverse: - # Compiler specs may lack EDSL implementations (e.g. unverified demos) - continue - errors.append( - f"{label}: {contract} has {subj_label} slots but no matching {ref_label} contract" - ) - continue - - ref_fields = reference[contract] - ref_map = {normalize_field_name(name): (ty, slot) for name, ty, slot in ref_fields} - subj_map = {normalize_field_name(name): (ty, slot) for name, ty, slot in subj_fields} - - for name, (s_ty, s_slot) in subj_map.items(): - if name not in ref_map: - errors.append( - f"{label}: {contract}.{name} in {subj_label} but not in {ref_label}" - ) - continue - - r_ty, r_slot = ref_map[name] - if s_slot != r_slot: - errors.append( - f"{label}: {contract}.{name} slot mismatch: " - f"{ref_label}={r_slot}, {subj_label}={s_slot}" - ) - if normalize_type(r_ty) != normalize_type(s_ty): - errors.append( - f"{label}: {contract}.{name} type mismatch: " - f"{ref_label}={r_ty}, {subj_label}={s_ty}" - ) - - if check_reverse: - for name in ref_map: - if name not in subj_map: - errors.append( - f"{label}: {contract}.{name} in {ref_label} but not in {subj_label}" - ) - - return errors - - def check_spec_edsl_consistency( edsl: dict[str, list[tuple[str, str, int]]], spec: dict[str, list[tuple[str, str, int]]], - compiler: dict[str, list[tuple[str, str, int]]], - compiler_externals: dict[str, bool], ) -> list[str]: - """Check Spec slot/type usage matches EDSL for compiled non-external contracts.""" + """Check Spec slot/type usage matches the EDSL for contracts with both layers. + + Smoke contracts (`*Smoke`) exercise macro features and are exempt. + """ errors: list[str] = [] - required_contracts = sorted( - c for c in compiler.keys() if not compiler_externals.get(c, False) - ) - for contract in required_contracts: - if contract not in edsl: - errors.append( - f"Spec-EDSL: {contract} in Compiler but missing from Contracts/" - ) - continue - if contract not in spec: - errors.append( - f"Spec-EDSL: {contract} in Compiler but missing Spec.lean slot usage" - ) + for contract in sorted(set(edsl) & set(spec)): + if contract.endswith("Smoke"): continue - edsl_slots = {(slot, normalize_type(ty)) for _name, ty, slot in edsl[contract]} spec_slots = {(slot, normalize_type(ty)) for _name, ty, slot in spec[contract]} @@ -443,66 +232,30 @@ def check_spec_edsl_consistency( return errors -def generate_report( - edsl: dict[str, list[tuple[str, str, int]]], - compiler: dict[str, list[tuple[str, str, int]]], - fmt: str = "text", -) -> str: +def generate_report(edsl: dict[str, list[tuple[str, str, int]]], fmt: str = "text") -> str: """Generate a storage layout report.""" lines = [] if fmt == "markdown": lines.append("## Storage Layout Report") lines.append("") - all_contracts = sorted(set(list(edsl.keys()) + list(compiler.keys()))) - for contract in all_contracts: + for contract in sorted(edsl): lines.append(f"### {contract}") lines.append("") - lines.append("| Slot | Field | Type | Layers |") - lines.append("|------|-------|------|--------|") - - edsl_fields = { - name: (ty, slot) for name, ty, slot in edsl.get(contract, []) - } - compiler_fields = { - name: (ty, slot) for name, ty, slot in compiler.get(contract, []) - } - all_fields = sorted( - set(list(edsl_fields.keys()) + list(compiler_fields.keys())), - key=lambda n: edsl_fields.get(n, compiler_fields.get(n, ("", 99)))[1], - ) - - for name in all_fields: - layers = [] - slot = "?" - ty = "?" - if name in edsl_fields: - layers.append("EDSL") - ty, slot = edsl_fields[name] - if name in compiler_fields: - layers.append("Compiler") - if slot == "?": - ty, slot = compiler_fields[name] - lines.append( - f"| {slot} | `{name}` | `{ty}` | {', '.join(layers)} |" - ) - + lines.append("| Slot | Field | Type |") + lines.append("|------|-------|------|") + for name, ty, slot in sorted(edsl[contract], key=lambda x: x[2]): + lines.append(f"| {slot} | `{name}` | `{ty}` |") lines.append("") else: lines.append("=" * 60) lines.append("STORAGE LAYOUT REPORT") lines.append("=" * 60) - all_contracts = sorted(set(list(edsl.keys()) + list(compiler.keys()))) - for contract in all_contracts: + for contract in sorted(edsl): lines.append("") lines.append(f" {contract}") - edsl_slots = edsl.get(contract, []) - for name, ty, slot in sorted(edsl_slots, key=lambda x: x[2]): + for name, ty, slot in sorted(edsl[contract], key=lambda x: x[2]): lines.append(f" Slot {slot}: {name} ({ty})") - if not edsl_slots: - compiler_slots = compiler.get(contract, []) - for name, ty, slot in sorted(compiler_slots, key=lambda x: x[2]): - lines.append(f" Slot {slot}: {name} ({ty}) [compiler only]") return "\n".join(lines) @@ -538,35 +291,16 @@ def main(): if slots: spec_all[contract_name] = slots - # 3. Extract Compiler specs - compiler_specs_file = ROOT / "Compiler" / "Specs.lean" - compiler_all: dict[str, list[tuple[str, str, int]]] = {} - if compiler_specs_file.exists(): - compiler_all = extract_compiler_specs(compiler_specs_file) - compiler_externals: dict[str, bool] = {} - if compiler_specs_file.exists(): - compiler_externals = extract_compiler_externals(compiler_specs_file) - - # 4. Check intra-contract collisions (EDSL/Spec/Compiler) + # 3. Check intra-contract collisions for contract, slots in edsl_all.items(): errors.extend(check_intra_collisions(contract, slots, "EDSL")) for contract, slots in spec_all.items(): errors.extend(check_intra_collisions(contract, slots, "Spec")) - for contract, slots in compiler_all.items(): - errors.extend(check_intra_collisions(contract, slots, "Compiler")) - - # 5. Check cross-layer consistency (EDSL ↔ Compiler) - errors.extend(check_layer_consistency( - edsl_all, compiler_all, "Cross-layer", - ref_label="EDSL", subj_label="Compiler", check_reverse=True, - )) - # 6. Check Spec-EDSL consistency (slot/type parity for compiled contracts) - errors.extend(check_spec_edsl_consistency( - edsl_all, spec_all, compiler_all, compiler_externals - )) + # 4. Check Spec-EDSL consistency + errors.extend(check_spec_edsl_consistency(edsl_all, spec_all)) - # 7. Report + # 5. Report fmt = "markdown" if "--format=markdown" in sys.argv else "text" if errors: @@ -577,7 +311,7 @@ def main(): sys.exit(1) print("Storage layout validation passed.\n") - print(generate_report(edsl_all, compiler_all, fmt)) + print(generate_report(edsl_all, fmt)) if __name__ == "__main__": diff --git a/scripts/check_storage_lens_freeze.py b/scripts/check_storage_lens_freeze.py index d4d1206af3..a3b9d86da2 100644 --- a/scripts/check_storage_lens_freeze.py +++ b/scripts/check_storage_lens_freeze.py @@ -65,7 +65,6 @@ # raw sites here; they are lens metatheory, not storage bypasses. "Verity/Core.lean": 33, # IRState fixture literals (field-name collision false positives). - "Contracts/TypedIRTests.lean": 3, } RECORD_UPDATE_RE = re.compile( diff --git a/scripts/check_struct_mapping_surface_sync.py b/scripts/check_struct_mapping_surface_sync.py deleted file mode 100644 index 87e205fefc..0000000000 --- a/scripts/check_struct_mapping_surface_sync.py +++ /dev/null @@ -1,27 +0,0 @@ -#!/usr/bin/env python3 -"""Deprecated shim: this checker now lives in scripts/docsync.py (entry `struct_mapping_surface`). - -Prefer `python3 scripts/docsync.py --check --only struct_mapping_surface`. -This wrapper is kept so existing callers of this path keep working. -""" - -from __future__ import annotations - -import sys -from pathlib import Path - -SCRIPT_DIR = Path(__file__).resolve().parent -if str(SCRIPT_DIR) not in sys.path: - sys.path.insert(0, str(SCRIPT_DIR)) - -from docsync import run_entry - -ENTRY = "struct_mapping_surface" - - -def main() -> int: - return run_entry(ENTRY) - - -if __name__ == "__main__": - raise SystemExit(main()) diff --git a/scripts/check_trust_surface_registry.py b/scripts/check_trust_surface_registry.py index dce2ca04ac..6e7d226c62 100644 --- a/scripts/check_trust_surface_registry.py +++ b/scripts/check_trust_surface_registry.py @@ -19,7 +19,7 @@ from property_utils import ROOT, report_errors, scrub_lean_code, strip_lean_comments SCAN_ROOTS = ("Compiler", "Verity", "Contracts", "Benchmark") -DOC_PATHS = ("AXIOMS.md", "TRUST_ASSUMPTIONS.md") +DOC_PATHS = ("docs/AXIOMS.md", "docs/TRUST_ASSUMPTIONS.md") DOC_DIRS = ("docs",) LEAN431_STRING_FACTS = Path("Compiler/CompilationModel/ReservedScratchNames.lean") diff --git a/scripts/consolidation-inventory.md b/scripts/consolidation-inventory.md deleted file mode 100644 index 452f5042a5..0000000000 --- a/scripts/consolidation-inventory.md +++ /dev/null @@ -1,276 +0,0 @@ -# Doc-Sync Consolidation Inventory (P7) - -Seed document for collapsing the ~31 `*sync*`/`check_*` doc-sync checkers and -~19 `generate_*` generators in `scripts/` into one schema-driven pipeline. - -Scope: inventory + design only. No checker is deleted or modified by this -document. Every script below remains authoritative until its replacement ships -and `make check` / CI parity is demonstrated. - -Sources: the `check:` target in `Makefile` (lines 125–170), `scripts/REFERENCE.md`, -and `.github/workflows/*.yml`. - -## 1. Full inventory - -Legend for **Source**: `lean-grep` = greps/parses Lean sources; `artifact` = -reads `artifacts/*.json`; `doc-grep` = parses Markdown docs; `lake` = runs a -Lean executable; `repo` = other repo files (workflows, lakefiles, foundry.toml). -**Output**: `exit` = exit code only; `regen` = writes a file, with `--check` -drift mode unless noted. - -### 1.1 Invoked by `make check` - -| Script | Guards (pair / invariant) | Source | Output | Shared helpers | -|---|---|---|---|---| -| check_property_manifest.py | `test/property_manifest.json` ↔ `Property*.t.sol` theorem names | artifact + Foundry tests | exit | property_utils | -| check_property_coverage.py | manifest entries ↔ test coverage ↔ exclusions.json | artifact + Foundry tests | exit | property_utils | -| check_property_manifest_sync.py | manifest ↔ actual Lean proof theorems | lean-grep + artifact | exit | property_utils | -| check_contract_structure.py | `Contracts/` layout (Spec/Invariants/Proofs files present) | lean-grep + fs | exit | property_utils | -| check_paths.py | case-insensitive checkout collisions; bridge quantification | `git ls-files` | exit | property_utils | -| check_compilationmodel_split.py | `Compiler/CompilationModel.lean` facade ↔ submodules | lean-grep | exit | property_utils | -| check_axioms.py | `AXIOMS.md` ↔ discovered Lean `axiom` decls; PrintAxioms output | lean-grep + doc-grep | exit | property_utils | -| check_trust_surface_registry.py | non-axiom trust mechanisms ↔ AXIOMS.md / TRUST_ASSUMPTIONS.md / docs | lean-grep + doc-grep | exit | property_utils | -| check_benchmark_cases.py | `Benchmark/Cases/*/case.yaml` metadata ↔ FormalAudit.lean presence | repo (yaml) + fs | exit | property_utils | -| generate_verification_status.py | `artifacts/verification_status.json` | lean-grep (via collect_metrics) | regen + `--check` | verification_metrics | -| generate_layer2_boundary_catalog.py | `artifacts/layer2_boundary_catalog.json` (static catalog) | hardcoded dict | regen + `--check` | property_utils | -| check_verification_status_doc.py | `docs/VERIFICATION_STATUS.md` ↔ verification_status.json | artifact + doc-grep | exit | verification_metrics, property_utils | -| check_layer2_boundary_sync.py | Layer-2 claims across README / AXIOMS.md / docs / docs-site | doc-grep (expected snippets) | exit | — | -| check_layer2_boundary_catalog_sync.py | Layer-2 docs ↔ layer2_boundary_catalog.json | artifact + doc-grep | exit | — | -| generate_verify_sync_spec.py | `scripts/verify_sync_spec.json` ↔ verify_sync_spec_source.py | python source | regen + `--check` | verify_sync_spec_source | -| check_verify_sync.py | verify.yml ↔ Makefile ↔ verify_sync_spec.json (jobs, paths, commands) | repo + artifact | exit | workflow_jobs | -| check_bridge_coverage_sync.py | bridge coverage docs ↔ native lowering report | artifact + doc-grep | exit | — | -| check_builtin_bridge_matrix_sync.py | builtin bridge docs ↔ interpreter_feature_matrix.json + lowering report | artifact + lean-grep + doc-grep | exit | interpreter_feature_matrix | -| check_interpreter_feature_boundary_catalog_sync.py | proof-boundary category note ↔ feature matrix | artifact + doc-grep | exit | interpreter_feature_matrix | -| check_interpreter_feature_summary_sync.py | `docs/INTERPRETER_FEATURE_MATRIX.md` summary table ↔ feature matrix counts | artifact + doc-grep | exit | interpreter_feature_matrix | -| check_low_level_call_boundary_sync.py | low-level-call boundary note ↔ feature matrix status | artifact + doc-grep | exit | interpreter_feature_matrix | -| check_linear_memory_boundary_sync.py | linear-memory boundary note ↔ feature matrix status | artifact + doc-grep | exit | interpreter_feature_matrix | -| check_axiomatized_primitive_boundary_sync.py | keccak256 axiomatized-primitive note ↔ feature matrix status | artifact + doc-grep | exit | interpreter_feature_matrix | -| check_struct_mapping_surface_sync.py | struct-mapping docs ↔ `Compiler/CompilationModel/Types.lean` surface | lean-grep + doc-grep | exit | — | -| check_solc_pin.py | solc pin across verify.yml / foundry.toml / TRUST_ASSUMPTIONS.md / official list.json hashes | repo + doc-grep; `--verify-published-checksums` fetches list.json | exit | official_solc | -| official_solc.py | committed binaries.soliditylang.org SHA-256 pins and list.json parsing | imported | — | — | -| setup_solc_importer.py | install official solc after list.json pin check | network + dest file | exit | official_solc | -| check_issue_templates.py | `.github/ISSUE_TEMPLATE/*.yaml` structure + log contamination | repo (yaml) | exit | property_utils | -| check_docs_workflow_sync.py | docs.yml push paths ↔ pull_request paths | repo (yaml) | exit | — | -| check_macro_health.py | dispatcher → check_macro_property_test_generation.py | sub-script | exit | — (dispatch) | -| check_storage_layout.py | storage slots: EDSL ↔ Spec ↔ Compiler layers | lean-grep | exit | property_utils | -| generate_storage_layout_report.py | `artifacts/storage_layout_report.json` + `STORAGE_LAYOUT_SUMMARY.md` | lake (`verity-storage-layout-report`) | regen + `--check --no-lean` | property_utils | -| check_lean_hygiene.py | no debug commands / sorry / unsafe reducibility in proofs | lean-grep | exit | property_utils | -| check_gas.py coverage | dispatcher → check_gas_model_coverage.py (also report/calibration subcmds) | sub-scripts | exit | — (dispatch) | -| check_compiler_boundaries.py | dispatcher → 7 boundary sub-checkers (builtin-list, contract/package/layer imports, mapping-slot, evmyullean-capability, package-glob) | sub-scripts | exit | — (dispatch) | -| check_split_compiler_test_artifacts.py | `packages/verity-compiler/lakefile.lean` globs ↔ `Compiler/*Test.lean` | repo + fs | exit | property_utils | -| check_yul.py --builtin-boundary-only | native bridge lemma presence for Yul builtins | lean-grep | exit | property_utils | -| check_rewrite_proof_metadata.py | rewrite rules ↔ proofId refs ↔ ParityPacks composition proofs | lean-grep | exit | property_utils | -| generate_evmyullean_capability_report.py | `artifacts/evmyullean_capability_report.json` + `evmyullean_unsupported_nodes.json` | lean-grep | regen + `--check` | evmyullean_capability | -| generate_evmyullean_native_lowering_report.py | `artifacts/evmyullean_native_lowering_report.json` | lean-grep (bridge files) | regen + `--check` | — | -| generate_evmyullean_fork_audit.py | `artifacts/evmyullean_fork_audit.json` ↔ lake-manifest pinned commit | repo + hardcoded FORK_AUDIT | regen + `--check` | — | -| generate_print_axioms.py | `PrintAxioms.lean` ↔ all theorem decls | lean-grep | regen + `--check` | property_utils | -| check_proof_length.py | proof line limits (soft 30 / hard 50, allowlist) | lean-grep | exit | property_utils | -| check_issue_1060_integrity.py | `artifacts/issue_1060_progress.json` ledger schema + repo facts | artifact + repo | exit | — | -| update_doc_numbers.py --check | docs-site/public/llms.txt metric markers ↔ verification_status.json | artifact + doc-grep | regen + `--check` | — | -| (unittest discover `test_*.py`) | unit coverage of the above checkers | — | exit | — | - -### 1.2 CI-only (workflows, not `make check`) - -| Script | Guards | Source | Output | Helpers | -|---|---|---|---|---| -| check_selectors.py | selector uniqueness / signature ↔ Yul labels / constants | lean-grep + Yul + keccak | exit | keccak256, property_utils, check_selector_fixtures | -| check_selector_fixtures.py | selector hashing ↔ `solc --hashes` fixtures | repo + solc | exit | keccak256 | -| check_lean_warning_regression.py | lake build warnings ↔ `artifacts/lean_warning_baseline.json` | build log + artifact | exit / `--write-baseline` | — | -| check_patch_gas_delta.py | gas regression: baseline vs patch TSV reports | TSV reports | exit (+ markdown summary) | — | -| check_split_package_builds.py | per-package lake builds succeed | lake | exit | — | -| check_issue_submission.py | issue-intake guard (issue body validation) | GitHub event | exit | — | -| check_macro_roundtrip_fuzz_coverage.py | macro roundtrip fuzz coverage | lean-grep | exit | — | -| report_property_coverage.py | coverage statistics report (informational) | artifact + tests | report (md/json/text) | property_utils | -| sync_verification_status_doc.py | writer twin of check_verification_status_doc (patches doc + hygiene baseline) | artifact | regen (no --check) | verification_metrics, property_utils | -| extract_property_manifest.py | writer twin of check_property_manifest_sync (emits manifest) | lean-grep | regen | property_utils | - -## 2. Consolidation clusters - -### Cluster A — "Lean decl → JSON artifact → Markdown doc" pipelines (16 scripts → 1) - -The dominant pattern: a generator scrapes Lean source (or runs a Lean exe) into -an `artifacts/*.json` snapshot, and one or more checkers grep Markdown docs to -assert they match that snapshot. - -Members: -- generate_verification_status.py / check_verification_status_doc.py / sync_verification_status_doc.py / update_doc_numbers.py -- generate_layer2_boundary_catalog.py / check_layer2_boundary_sync.py / check_layer2_boundary_catalog_sync.py -- generate_evmyullean_capability_report.py / generate_evmyullean_native_lowering_report.py / generate_evmyullean_fork_audit.py / check_bridge_coverage_sync.py -- check_builtin_bridge_matrix_sync.py / check_interpreter_feature_boundary_catalog_sync.py / check_interpreter_feature_summary_sync.py -- generate_storage_layout_report.py -- generate_print_axioms.py - -Proposal: **`scripts/docsync.py`** driven by a declarative schema -(`scripts/docsync_spec.json` or a Python source module mirroring the -`verify_sync_spec_source.py` pattern, which is already the house style for -schema-driven checking). Each pipeline entry declares: - -``` -{ "id": "verification_status", - "extract": {"kind": "lean-grep" | "lake-exe" | "static" | "python-fn", ...}, - "artifact": "artifacts/verification_status.json", - "doc_bindings": [ {"doc": "docs/VERIFICATION_STATUS.md", - "binding": "marker" | "table" | "snippet" | "count", - ...} ] } -``` - -Modes: `docsync.py generate [id...]` (replaces all `generate_*` + `sync_*` + -`update_doc_numbers.py` writers), `docsync.py check [id...]` (replaces the -paired checkers; exit-code semantics identical to today). The -`interpreter_feature_matrix.py` loader becomes one `extract.kind`. - -### Cluster B — conditional proof-boundary note checkers (4 scripts → schema rows in Cluster A) - -check_low_level_call_boundary_sync.py, check_linear_memory_boundary_sync.py, -check_axiomatized_primitive_boundary_sync.py, -check_struct_mapping_surface_sync.py. - -These are already near-identical thin wrappers: read -`artifacts/interpreter_feature_matrix.json` (or one Lean file), decide whether -a boundary note is required, then grep docs for the note. They differ only in -feature keys and note text. Proposal: a `"binding": "conditional-note"` row -type in the Cluster A schema (`when: feature X not fully proved → require -snippet S in docs D1..Dn`). 4 scripts collapse into 4 JSON rows. - -### Cluster C — property manifest / coverage family (5 scripts → 1) - -check_property_manifest.py, check_property_coverage.py, -check_property_manifest_sync.py, extract_property_manifest.py, -report_property_coverage.py. - -All are already thin consumers of `property_utils.py` (load_manifest, -collect_covered, THEOREM_RE...). Proposal: **`scripts/property_pipeline.py`** -with subcommands `extract`, `check` (manifest + coverage + lean-sync in one -pass — they currently re-parse the same files three times), `report`. -property_utils.py stays as the shared library. - -### Cluster D — cross-file constant/pin sync checks (4 scripts → 1) - -check_solc_pin.py, check_docs_workflow_sync.py, check_builtin_list_sync.py -(via check_compiler_boundaries), generate_verify_sync_spec.py + -check_verify_sync.py. - -Pattern: assert the same literal value/list appears consistently across N repo -files (workflow yaml, foundry.toml, Lean string lists, Makefile). Proposal: -extend the existing **check_verify_sync.py table-driven validator** (and -`verify_sync_spec_source.py` contract) with generic "literal pin" and -"list-equality" rule kinds; solc-pin, docs-workflow paths, and -builtin-list become spec entries. check_verify_sync.py is the surviving -binary; `workflow_jobs.py` remains its parser library. - -### Cluster E — Lean structure/hygiene lints (9 scripts → 1 + rule modules) - -check_contract_structure.py, check_compilationmodel_split.py, -check_lean_hygiene.py, check_proof_length.py, check_paths.py, -check_storage_layout.py, check_rewrite_proof_metadata.py, -check_split_compiler_test_artifacts.py, check_trust_surface_registry.py and -check_axioms.py (the lean-grep halves). - -Pure lints: grep Lean sources, no artifact/doc pairing. They share -property_utils scrubbers but each re-walks the tree. Proposal: -**`scripts/lean_lint.py`** — one tree walk, pluggable rule registry, `--only` / -`--skip` flags matching the current dispatcher conventions -(check_compiler_boundaries.py / check_macro_health.py / check_gas.py already -prove the dispatcher pattern works; lean_lint generalizes it with a single -parse pass instead of N subprocesses). - -### Cluster F — external-toolchain checks (keep separate) - -check_selectors.py, check_selector_fixtures.py, check_yul.py, check_gas.py -family, check_patch_gas_delta.py, check_lean_warning_regression.py, -check_split_package_builds.py, check_benchmark_cases.py, -check_issue_templates.py, check_issue_1060_integrity.py, -check_issue_submission.py. - -These depend on solc/Foundry/lake build logs/GitHub events rather than the -Lean↔artifact↔doc axis. Consolidating them buys little and risks much; out of -scope for the doc-sync pipeline. (check_gas.py already is its own dispatcher.) - -## 2.1 Migrated so far (P7 status) - -`scripts/docsync.py` (declarative entry registry + engine, `--check` / `--only` / -`--list`) has landed. Migrated entries — each legacy script is now a thin shim -that delegates to docsync, its Makefile line invokes -`python3 scripts/docsync.py --check --only `, and its unit tests target -docsync directly: - -- [x] check_low_level_call_boundary_sync.py → `low_level_call_boundary` (Cluster B) -- [x] check_linear_memory_boundary_sync.py → `linear_memory_boundary` (Cluster B) -- [x] check_axiomatized_primitive_boundary_sync.py → `axiomatized_primitive_boundary` (Cluster B) -- [x] check_struct_mapping_surface_sync.py → `struct_mapping_surface` (Cluster B) -- [x] check_layer2_boundary_sync.py → `layer2_boundary` (Cluster A) -- [x] check_layer2_boundary_catalog_sync.py → `layer2_boundary_catalog` (Cluster A) -- [x] check_interpreter_feature_boundary_catalog_sync.py → `interpreter_feature_boundary_catalog` (Cluster A) - -Cluster C landed as `scripts/property_pipeline.py` (subcommands `check -[--only manifest|coverage|lean-sync]`, `extract`, `report`; one manifest parse -for all three checks). The legacy scripts are thin shims; the three Makefile -check lines collapsed into `python3 scripts/property_pipeline.py check`: - -- [x] check_property_manifest.py → `property_pipeline.py check --only manifest` (Cluster C) -- [x] check_property_coverage.py → `property_pipeline.py check --only coverage` (Cluster C) -- [x] check_property_manifest_sync.py → `property_pipeline.py check --only lean-sync` (Cluster C) -- [x] extract_property_manifest.py → `property_pipeline.py extract` (Cluster C) -- [x] report_property_coverage.py → `property_pipeline.py report` (Cluster C) - -Cluster E landed as `scripts/lean_lint.py` (rule registry with `--only` / -`--skip` / `--list`, generalizing the check_compiler_boundaries.py dispatcher -pattern). The legacy scripts stay as the rule modules — their logic, output, -and exit codes are untouched (verified byte-identical per rule), so the -arg-bearing CI invocations (`check_axioms.py --log`, `check_proof_length.py ---format=markdown`, `check_storage_layout.py --format=markdown`) keep calling -them directly; `make check` goes through lean_lint: - -- [x] check_contract_structure.py → `lean_lint.py --only contract_structure` (Cluster E) -- [x] check_paths.py → `lean_lint.py --only paths` (Cluster E) -- [x] check_compilationmodel_split.py → `lean_lint.py --only compilationmodel_split` (Cluster E) -- [x] check_axioms.py → `lean_lint.py --only axioms` (Cluster E) -- [x] check_trust_surface_registry.py → `lean_lint.py --only trust_surface_registry` (Cluster E) -- [x] check_storage_layout.py → `lean_lint.py --only storage_layout` (Cluster E) -- [x] check_lean_hygiene.py → `lean_lint.py --only lean_hygiene` (Cluster E; module invoked unmodified — empty sorry allowlist, Smoke native_decide carve-out, and debug-command rules unchanged) -- [x] check_split_compiler_test_artifacts.py → `lean_lint.py --only split_compiler_test_artifacts` (Cluster E) -- [x] check_rewrite_proof_metadata.py → `lean_lint.py --only rewrite_proof_metadata` (Cluster E) -- [x] check_proof_length.py → `lean_lint.py --only proof_length` (Cluster E) - -Cluster E note: the single-tree-walk unification (one parse pass shared across -rules) is deliberately deferred — the rules' scrubbing/walk scopes differ -(Proofs-only vs whole tree vs Contracts layout) and check_lean_hygiene.py is a -trust gate, so the dispatcher keeps each rule's parsing byte-exact; sharing the -walk can land later behind the same `lean_lint.py` interface without touching -callers. - -Still pending from Cluster A: the verification-status trio + -update_doc_numbers.py, the layer2 catalog generator, the EVMYulLean report -generators, builtin-bridge / feature-summary matrix checkers, -generate_storage_layout_report.py, generate_print_axioms.py. -`verify_sync_spec_source.py` pins the new docsync Makefile commands -(regenerate `verify_sync_spec.json` after any further migration). - -## 3. Summary - -| Cluster | Today | Proposed | Surviving entry point | -|---|---|---|---| -| A. Lean→artifact→doc pipelines | 16 | 1 | `docsync.py` + `docsync_spec` | -| B. Conditional boundary notes | 4 | 0 (schema rows in A) | — | -| C. Property manifest family | 5 | 1 | `property_pipeline.py` | -| D. Constant/pin sync | 5 | 1 | `check_verify_sync.py` (extended spec) | -| E. Lean structure lints | 10 | 1 | `lean_lint.py` | -| F. External toolchain | 11 | 11 (unchanged) | as-is | -| **Total in scope (A–E)** | **40** | **4** | | - -Already-thin wrappers over shared helpers (lowest-risk migrations first): -- `interpreter_feature_matrix.py` consumers (Cluster B + the three matrix-sync checkers). -- `property_utils.py` consumers in Cluster C. -- `verification_metrics.py` trio (generate/check/sync verification status). -- The dispatcher scripts (check_compiler_boundaries.py, check_macro_health.py, check_gas.py) — proof that the `--only/--skip` consolidation UX is already accepted. - -Migration constraints: -1. Each replaced checker must keep an alias shim (or Makefile line) until - `verify_sync_spec.json` is updated, since check_verify_sync.py pins the - Makefile/workflow command lists. -2. `make check` exit-code semantics and per-check error messages must be - preserved verbatim where tests (`test_check_*.py`) assert on them; the unit - tests migrate cluster-by-cluster. -3. AUDIT.md / TRUST_ASSUMPTIONS.md / AXIOMS.md sync rules (CLAUDE.md - non-negotiable #1) apply when Cluster A lands, since it moves trust-surface - checking into a schema. diff --git a/scripts/docsync.py b/scripts/docsync.py index aa08bd54b5..c9d8ab3fad 100644 --- a/scripts/docsync.py +++ b/scripts/docsync.py @@ -2,8 +2,7 @@ """Schema-driven doc-sync engine (P7 consolidation). One declarative registry replaces the per-checker scripts of the -"Lean decl -> JSON artifact -> Markdown doc" pipeline described in -scripts/consolidation-inventory.md (Clusters A/B). Each entry binds an +"Lean decl -> JSON artifact -> Markdown doc" pipeline. Each entry binds an extraction condition (interpreter feature matrix, Lean source surface, or static expectations) to required/forbidden snippets in Markdown docs. @@ -572,12 +571,12 @@ def check(self, root: Path) -> int: LAYER2_BOUNDARY = SnippetSyncEntry( name="layer2_boundary", targets={ - "AXIOMS": "AXIOMS.md", + "AXIOMS": "docs/AXIOMS.md", "COMPILER_PROOFS_README": "Compiler/Proofs/README.md", "VERIFICATION_STATUS": "docs/VERIFICATION_STATUS.md", "ROADMAP": "docs/ROADMAP.md", "ROOT_README": "README.md", - "TRUST_ASSUMPTIONS": "TRUST_ASSUMPTIONS.md", + "TRUST_ASSUMPTIONS": "docs/TRUST_ASSUMPTIONS.md", "DOCS_SITE_COMPILER": "docs-site/content/compiler.mdx", "DOCS_SITE_INDEX": "docs-site/content/index.mdx", "DOCS_SITE_EXAMPLES": "docs-site/content/examples.mdx", diff --git a/scripts/fetch_prebuilt_compiler_artifacts.sh b/scripts/fetch_prebuilt_compiler_artifacts.sh deleted file mode 100755 index b3a56be4a5..0000000000 --- a/scripts/fetch_prebuilt_compiler_artifacts.sh +++ /dev/null @@ -1,73 +0,0 @@ -#!/usr/bin/env bash -set -euo pipefail - -workflow="${WORKFLOW:-verify.yml}" -ref="${1:-$(git branch --show-current 2>/dev/null || echo main)}" -dest="${2:-.}" -run_id="${RUN_ID:-}" - -require_cmd() { - command -v "$1" >/dev/null 2>&1 || { - echo "missing required command: $1" >&2 - exit 1 - } -} - -find_artifact_dir() { - local root="$1" - local suffix="$2" - local hit - hit="$(find "$root" -type d -path "*/$suffix" | head -n 1 || true)" - if [ -n "$hit" ]; then - printf '%s\n' "$hit" - else - printf '%s\n' "$root" - fi -} - -require_cmd gh -require_cmd git - -repo="$(gh repo view --json nameWithOwner --jq .nameWithOwner)" -if [ -z "$run_id" ]; then - run_id="$( - gh run list \ - --repo "$repo" \ - --workflow "$workflow" \ - --branch "$ref" \ - --json databaseId,status,conclusion \ - --jq 'map(select(.status == "completed" and .conclusion == "success")) | .[0].databaseId' - )" -fi - -if [ -z "$run_id" ] || [ "$run_id" = "null" ]; then - echo "no successful $workflow run found for ref '$ref'" >&2 - exit 1 -fi - -tmp_dir="$(mktemp -d)" -trap 'rm -rf "$tmp_dir"' EXIT - -echo "Downloading artifacts from run $run_id in $repo" -gh run download "$run_id" --repo "$repo" -n generated-yul -D "$tmp_dir/generated-yul" -gh run download "$run_id" --repo "$repo" -n generated-yul-patched -D "$tmp_dir/generated-yul-patched" -gh run download "$run_id" --repo "$repo" -n verity-compiler-binaries -D "$tmp_dir/verity-compiler-binaries" - -mkdir -p "$dest/compiler/yul" "$dest/compiler/yul-patched" "$dest/.lake/build/bin" - -baseline_src="$(find_artifact_dir "$tmp_dir/generated-yul" "compiler/yul")" -patched_src="$(find_artifact_dir "$tmp_dir/generated-yul-patched" "compiler/yul-patched")" -bin_src="$(find_artifact_dir "$tmp_dir/verity-compiler-binaries" "compiler/bin")" - -cp -R "$baseline_src/." "$dest/compiler/yul/" -cp -R "$patched_src/." "$dest/compiler/yul-patched/" -cp -R "$bin_src/." "$dest/.lake/build/bin/" - -chmod +x \ - "$dest/.lake/build/bin/verity-compiler" \ - "$dest/.lake/build/bin/difftest-interpreter" - -echo "Restored compiler artifacts into:" -echo " $dest/compiler/yul" -echo " $dest/compiler/yul-patched" -echo " $dest/.lake/build/bin" diff --git a/scripts/generate_contract.py b/scripts/generate_contract.py index 34cc8895c4..1f5d2594a5 100755 --- a/scripts/generate_contract.py +++ b/scripts/generate_contract.py @@ -1221,14 +1221,8 @@ def {name_lower}Spec : CompilationModel := {{ def gen_all_lean_imports(cfg: ContractConfig) -> str: - """Generate import lines for Verity/All.lean.""" - return f""" -import Contracts.{cfg.name}.{cfg.name} -import Contracts.{cfg.name}.Spec -import Contracts.{cfg.name}.Invariants -import Contracts.{cfg.name}.Proofs -import Contracts.{cfg.name}.Proofs.Basic -import Contracts.{cfg.name}.Proofs.Correctness""" + """Generate the import line to add to Contracts.lean.""" + return f"import Contracts.{cfg.name}" def scaffold_files(cfg: ContractConfig) -> List[tuple[Path, str]]: @@ -1344,7 +1338,7 @@ def _fn_repr(fn: Function) -> str: print("=" * 60) print() - print("1. Add imports to Verity/All.lean:") + print("1. Add the import to Contracts.lean:") print(gen_all_lean_imports(cfg)) print() diff --git a/scripts/generate_print_axioms.py b/scripts/generate_print_axioms.py index f97e21420a..26d3508000 100644 --- a/scripts/generate_print_axioms.py +++ b/scripts/generate_print_axioms.py @@ -21,9 +21,6 @@ PROOF_DIRS = [ROOT / "Verity" / "Proofs", ROOT / "Compiler" / "Proofs"] TRUST_BOUNDARY_FILES = [ ROOT / "Compiler" / "CompilationModel" / "ReservedScratchNames.lean", - ROOT / "Contracts" / "SolidityImportSmoke" / "Inheritance" / "Proofs.lean", - ROOT / "Contracts" / "SolidityImportSmoke" / "Modifiers" / "Proofs.lean", - ROOT / "Contracts" / "SolidityImportSmoke" / "Structs" / "Proofs.lean", ] def _collect_contract_proof_dirs() -> list[Path]: @@ -153,12 +150,7 @@ def generate() -> str: # as the proof files are brought fully in sync. EXCLUDED_PATHS = { # Pre-existing proof failures (simp/tactic issues, API changes) - "Compiler/Proofs/YulGeneration/Lemmas.lean", # unsolved goals in selector proof - "Compiler/Proofs/YulGeneration/StatementEquivalence.lean", # switch keyword + unfold failures "Compiler/Proofs/IRGeneration/Expr.lean", # deep API mismatches (.setMapping, evmModulus) - # Transitive dependents of the above - "Compiler/Proofs/YulGeneration/Codegen.lean", # imports Lemmas - "Compiler/Proofs/YulGeneration/Preservation.lean", # imports Codegen + StatementEquivalence } all_files = [ f for f in all_files diff --git a/scripts/generate_storage_layout_report.py b/scripts/generate_storage_layout_report.py index c166343a4a..2e0ce25ed0 100644 --- a/scripts/generate_storage_layout_report.py +++ b/scripts/generate_storage_layout_report.py @@ -123,7 +123,7 @@ def render_summary(report: dict) -> str: "the generator after any change to a production storage layout._", "", "Source of truth: `Compiler.Specs.allSpecs` (canonical production", - "contract surface). See `AUDIT.md` for the audit-artifact registry", + "contract surface). See `docs/TRUST_ASSUMPTIONS.md` for the audit-artifact registry", "and issue #1897 for the migration-review motivation.", "", "## Non-alias certificate justifications (#1966)", diff --git a/scripts/generate_trust_surface_report.py b/scripts/generate_trust_surface_report.py index 56f64c4616..f0f5075858 100644 --- a/scripts/generate_trust_surface_report.py +++ b/scripts/generate_trust_surface_report.py @@ -52,7 +52,7 @@ def build_report() -> dict: "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 " + "registry: docs/AXIOMS.md, docs/TRUST_ASSUMPTIONS.md (enforced by " "scripts/check_trust_surface_registry.py)." ), } diff --git a/scripts/generate_yul_identity_diff_report.py b/scripts/generate_yul_identity_diff_report.py deleted file mode 100644 index eacdd6251c..0000000000 --- a/scripts/generate_yul_identity_diff_report.py +++ /dev/null @@ -1,259 +0,0 @@ -#!/usr/bin/env python3 -"""Generate a deterministic machine-readable Yul identity diff report. - -Compares Verity-generated Yul files against solc-generated Yul files and emits a -stable JSON report with mismatch localization to function/subtree paths. -""" - -from __future__ import annotations - -import argparse -import json -import re -import sys -from dataclasses import dataclass -from pathlib import Path -from typing import Dict, Iterable, List, Tuple - -ROOT = Path(__file__).resolve().parents[1] -DEFAULT_OUTPUT = ROOT / "artifacts" / "yul_identity_diff_report.json" - -FUNC_RE = re.compile(r"^\s*function\s+([A-Za-z_$][A-Za-z0-9_$]*)\s*\(") - - -@dataclass(frozen=True) -class NormLine: - depth: int - text: str - - -def parse_args(argv: list[str] | None = None) -> argparse.Namespace: - parser = argparse.ArgumentParser(description=__doc__) - parser.add_argument("--verity-dir", required=True, help="Directory containing Verity Yul files") - parser.add_argument("--solc-dir", required=True, help="Directory containing solc Yul files") - parser.add_argument( - "--output", - default=str(DEFAULT_OUTPUT), - help=f"Output JSON path (default: {DEFAULT_OUTPUT.relative_to(ROOT)})", - ) - parser.add_argument( - "--fail-on-mismatch", - action="store_true", - help="Exit non-zero when non-identical output is detected", - ) - parser.add_argument( - "--max-mismatches", - type=int, - default=1, - help="Allow up to this many mismatches when --fail-on-mismatch is used (temporary default=1 to tolerate the current single Yul identity delta on the 1971 branch until full dynamic/composite ABI work in #1982 lands and makes output identical again).", - ) - return parser.parse_args(argv) - - -def _strip_line_comment(line: str) -> str: - return line.split("//", 1)[0] - - -def _normalize_line(line: str) -> str: - line = _strip_line_comment(line).strip() - if not line: - return "" - return " ".join(line.split()) - - -def _collect_yul_files(base: Path) -> Dict[str, Path]: - result: Dict[str, Path] = {} - if not base.exists(): - return result - for path in sorted(base.rglob("*.yul")): - rel = path.relative_to(base).as_posix() - result[rel] = path - return result - - -def _extract_scoped_lines(text: str) -> Dict[str, List[NormLine]]: - scoped: Dict[str, List[NormLine]] = {"": []} - current_scope = "" - function_depth: int | None = None - depth = 0 - - for raw in text.splitlines(): - line = _normalize_line(raw) - if not line: - depth += raw.count("{") - depth -= raw.count("}") - continue - - match = FUNC_RE.match(line) - if match: - current_scope = f"function:{match.group(1)}" - scoped.setdefault(current_scope, []) - function_depth = depth - - scoped.setdefault(current_scope, []).append(NormLine(depth=max(depth, 0), text=line)) - - depth += raw.count("{") - depth -= raw.count("}") - - if function_depth is not None and depth <= function_depth: - current_scope = "" - function_depth = None - - return scoped - - -def _compare_sequences( - rel_path: str, - scope: str, - lhs: List[NormLine], - rhs: List[NormLine], -) -> List[dict]: - mismatches: List[dict] = [] - max_len = max(len(lhs), len(rhs)) - for idx in range(max_len): - left = lhs[idx] if idx < len(lhs) else None - right = rhs[idx] if idx < len(rhs) else None - if left is None: - mismatches.append( - { - "file": rel_path, - "path": f"{scope}/line:{idx + 1}", - "kind": "missing_in_verity", - "verity": None, - "solc": right.text, - } - ) - continue - if right is None: - mismatches.append( - { - "file": rel_path, - "path": f"{scope}/line:{idx + 1}", - "kind": "missing_in_solc", - "verity": left.text, - "solc": None, - } - ) - continue - if left.text != right.text: - mismatches.append( - { - "file": rel_path, - "path": f"{scope}/depth:{left.depth}/line:{idx + 1}", - "kind": "line_mismatch", - "verity": left.text, - "solc": right.text, - } - ) - return mismatches - - -def _compare_file(rel_path: str, verity_text: str, solc_text: str) -> List[dict]: - mismatches: List[dict] = [] - v_scopes = _extract_scoped_lines(verity_text) - s_scopes = _extract_scoped_lines(solc_text) - - all_scopes = sorted(set(v_scopes.keys()) | set(s_scopes.keys())) - for scope in all_scopes: - v_seq = v_scopes.get(scope) - s_seq = s_scopes.get(scope) - if v_seq is None: - mismatches.append( - { - "file": rel_path, - "path": scope, - "kind": "scope_missing_in_verity", - "verity": None, - "solc": "present", - } - ) - continue - if s_seq is None: - mismatches.append( - { - "file": rel_path, - "path": scope, - "kind": "scope_missing_in_solc", - "verity": "present", - "solc": None, - } - ) - continue - mismatches.extend(_compare_sequences(rel_path, scope, v_seq, s_seq)) - - return mismatches - - -def build_report(verity_dir: Path, solc_dir: Path) -> dict: - verity_files = _collect_yul_files(verity_dir) - solc_files = _collect_yul_files(solc_dir) - - all_files = sorted(set(verity_files.keys()) | set(solc_files.keys())) - mismatches: List[dict] = [] - - for rel in all_files: - v_path = verity_files.get(rel) - s_path = solc_files.get(rel) - if v_path is None: - mismatches.append( - { - "file": rel, - "path": "", - "kind": "file_missing_in_verity", - "verity": None, - "solc": "present", - } - ) - continue - if s_path is None: - mismatches.append( - { - "file": rel, - "path": "", - "kind": "file_missing_in_solc", - "verity": "present", - "solc": None, - } - ) - continue - mismatches.extend(_compare_file(rel, v_path.read_text(encoding="utf-8"), s_path.read_text(encoding="utf-8"))) - - summary: Dict[str, int] = {} - for mismatch in mismatches: - kind = mismatch["kind"] - summary[kind] = summary.get(kind, 0) + 1 - - return { - "tool": {"name": "generate_yul_identity_diff_report", "schemaVersion": 1}, - "inputs": {"verityDir": str(verity_dir), "solcDir": str(solc_dir)}, - "status": "identical" if not mismatches else "non_identical", - "summary": { - "filesCompared": len(all_files), - "totalMismatches": len(mismatches), - "byKind": dict(sorted(summary.items())), - }, - "mismatches": mismatches, - } - - -def write_report(report: dict, output: Path) -> None: - output.parent.mkdir(parents=True, exist_ok=True) - output.write_text(json.dumps(report, indent=2, sort_keys=True) + "\n", encoding="utf-8") - - -def main(argv: list[str] | None = None) -> int: - args = parse_args(argv) - report = build_report(Path(args.verity_dir), Path(args.solc_dir)) - write_report(report, Path(args.output)) - print(f"Wrote Yul identity report: {args.output}") - print(f"Status: {report['status']} ({report['summary']['totalMismatches']} mismatches)") - if args.fail_on_mismatch and report["summary"]["totalMismatches"] > args.max_mismatches: - return 1 - # For local make check / CI on this PR branch, never fail the target on the - # known 1 Yul mismatch (tolerated until full 1982 dynamic ABI work lands). - # The --fail-on-mismatch + --max-mismatches can be used for strict runs. - return 0 - - -if __name__ == "__main__": - raise SystemExit(main()) diff --git a/scripts/lean_lint.py b/scripts/lean_lint.py index ad86c789df..9fd2f3c1b0 100644 --- a/scripts/lean_lint.py +++ b/scripts/lean_lint.py @@ -35,7 +35,6 @@ "split_compiler_test_artifacts": "check_split_compiler_test_artifacts", "rewrite_proof_metadata": "check_rewrite_proof_metadata", "proof_length": "check_proof_length", - "spec_named_storage": "check_spec_named_storage", } diff --git a/scripts/setup_solc_import.py b/scripts/setup_solc_import.py new file mode 100644 index 0000000000..19883700b4 --- /dev/null +++ b/scripts/setup_solc_import.py @@ -0,0 +1,95 @@ +#!/usr/bin/env python3 +"""Install pinned solc 0.8.34 for the Solidity importer (`solidity_import`). + +The binary is written to `.lake/solidity-import/solc-0.8.34`. +Lake elaboration never downloads the compiler. +""" + +from __future__ import annotations + +import argparse +import hashlib +import platform +import sys +import urllib.request +from pathlib import Path + +ROOT = Path(__file__).resolve().parents[1] +DEST = ROOT / ".lake" / "solidity-import" / "solc-0.8.34" +LONG_VERSION = "0.8.34+commit.80d5c536" +SHA256 = { + "linux-amd64": "d40adc6f9fdbb22a97d32a02fa05688bf2ee7886affc48c9851b0afd4a726b39", + "macosx-amd64": "0a2829292697dda542e4e365bb63fbd6d3ed51537140222a880ab760cffa7746", +} + + +def host_platform() -> str: + if sys.platform == "darwin": + return "macosx-amd64" + if sys.platform.startswith("linux") and platform.machine() in ("x86_64", "amd64"): + return "linux-amd64" + raise RuntimeError(f"no solc 0.8.34 pin for {sys.platform} {platform.machine()}") + + +def file_sha256(path: Path) -> str: + digest = hashlib.sha256() + with path.open("rb") as handle: + for chunk in iter(lambda: handle.read(1024 * 1024), b""): + digest.update(chunk) + return digest.hexdigest() + + +def install(platform_name: str, dest: Path) -> None: + expected = SHA256[platform_name] + list_url = f"https://binaries.soliditylang.org/{platform_name}/list.json" + request = urllib.request.Request(list_url, headers={"User-Agent": "verity-official-solc/0.8.34"}) + import json + with urllib.request.urlopen(request, timeout=60) as response: + payload = json.load(response) + match = None + for entry in payload.get("builds", []): + if entry.get("longVersion") == LONG_VERSION: + match = entry + break + if match is None: + raise RuntimeError(f"list.json has no build for {LONG_VERSION}") + published = str(match["sha256"]) + if published.startswith("0x"): + published = published[2:] + if published != expected: + raise RuntimeError(f"list.json SHA-256 {published} != pin {expected}") + url = f"https://binaries.soliditylang.org/{platform_name}/{match['path']}" + dest.parent.mkdir(parents=True, exist_ok=True) + tmp = dest.with_name(dest.name + ".tmp") + try: + binary = urllib.request.Request(url, headers={"User-Agent": "verity-official-solc/0.8.34"}) + with urllib.request.urlopen(binary, timeout=60) as response, tmp.open("wb") as handle: + handle.write(response.read()) + got = file_sha256(tmp) + if got != expected: + raise RuntimeError(f"downloaded SHA-256 {got} != {expected}") + tmp.replace(dest) + dest.chmod(0o755) + finally: + if tmp.exists(): + tmp.unlink() + print(f"installed {LONG_VERSION} ({platform_name}) at {dest}") + + +def main() -> int: + parser = argparse.ArgumentParser(description=__doc__) + parser.add_argument("--output", type=Path, default=DEST, help="destination binary path") + dest = parser.parse_args().output + if dest.exists() and file_sha256(dest) == SHA256[host_platform()]: + print(f"already installed at {dest}") + return 0 + install(host_platform(), dest) + return 0 + + +if __name__ == "__main__": + try: + raise SystemExit(main()) + except Exception as exc: + print(f"error: {exc}", file=sys.stderr) + raise SystemExit(1) diff --git a/scripts/setup_solc_importer.py b/scripts/setup_solc_importer.py deleted file mode 100644 index 8eaed0b5c2..0000000000 --- a/scripts/setup_solc_importer.py +++ /dev/null @@ -1,98 +0,0 @@ -#!/usr/bin/env python3 -"""Install the official pinned solc for the Lean Solidity importer. - -Fetches binaries.soliditylang.org `/list.json`, checks that the -published 0.8.33 SHA-256 still matches the committed pin, then downloads that -build to `.lake/solidity-import/solc`. Lake never performs this fetch. -""" - -from __future__ import annotations - -import argparse -import hashlib -import platform -import sys -import urllib.request -from pathlib import Path - -SCRIPT_DIR = Path(__file__).resolve().parent -if str(SCRIPT_DIR) not in sys.path: - sys.path.insert(0, str(SCRIPT_DIR)) - -import official_solc - -ROOT = Path(__file__).resolve().parents[1] -DEFAULT_DEST = ROOT / ".lake" / "solidity-import" / "solc" - - -def host_platform() -> str: - if sys.platform == "darwin": - return "macosx-amd64" - if sys.platform.startswith("linux") and platform.machine() in ("x86_64", "amd64"): - return "linux-amd64" - raise RuntimeError( - f"no official solc pin for {sys.platform} {platform.machine()}" - ) - - -def file_sha256(path: Path) -> str: - digest = hashlib.sha256() - with path.open("rb") as handle: - for chunk in iter(lambda: handle.read(1024 * 1024), b""): - digest.update(chunk) - return digest.hexdigest() - - -def install(dest: Path, platform_name: str) -> None: - expected = official_solc.OFFICIAL_SOLC_SHA256[platform_name] - published = official_solc.published_build(official_solc.fetch_list(platform_name)) - if published["sha256"] != expected: - raise RuntimeError( - f"{platform_name} list.json SHA-256 {published['sha256']} " - f"does not match committed pin {expected}" - ) - dest.parent.mkdir(parents=True, exist_ok=True) - url = official_solc.binary_url(platform_name, published["path"]) - tmp = dest.with_name(dest.name + ".tmp") - try: - request = urllib.request.Request( - url, - headers={"User-Agent": "verity-official-solc/0.8.33"}, - ) - with urllib.request.urlopen(request, timeout=60) as response, tmp.open("wb") as handle: - handle.write(response.read()) - got = file_sha256(tmp) - if got != expected: - raise RuntimeError(f"downloaded {url} SHA-256 {got} != {expected}") - tmp.replace(dest) - dest.chmod(0o755) - finally: - if tmp.exists(): - tmp.unlink() - print(f"installed official solc {official_solc.SOLC_LONG_VERSION} ({platform_name}) at {dest}") - - -def main() -> int: - parser = argparse.ArgumentParser(description=__doc__) - parser.add_argument( - "--dest", - type=Path, - default=DEFAULT_DEST, - help="destination path (default: .lake/solidity-import/solc)", - ) - parser.add_argument( - "--platform", - choices=sorted(official_solc.OFFICIAL_SOLC_SHA256), - help="override host platform (default: detect)", - ) - args = parser.parse_args() - try: - install(args.dest, args.platform or host_platform()) - except (RuntimeError, ValueError, OSError) as err: - print(f"solc importer setup failed: {err}", file=sys.stderr) - return 1 - return 0 - - -if __name__ == "__main__": - raise SystemExit(main()) diff --git a/scripts/solidity_differential/Runner.t.sol b/scripts/solidity_differential/Runner.t.sol new file mode 100644 index 0000000000..e1a2e18877 --- /dev/null +++ b/scripts/solidity_differential/Runner.t.sol @@ -0,0 +1,80 @@ +// SPDX-License-Identifier: MIT +pragma solidity 0.8.34; + +interface VmSlice { + function readFile(string calldata) external view returns (string memory); + function readFileBinary(string calldata) external view returns (bytes memory); + function writeFile(string calldata, string calldata) external; + function writeLine(string calldata, string calldata) external; + function parseJsonString(string calldata, string calldata) external pure returns (string memory); + function parseJsonUint(string calldata, string calldata) external pure returns (uint256); + function parseJsonBytes(string calldata, string calldata) external pure returns (bytes memory); + function parseUint(string calldata) external pure returns (uint256); + function toString(uint256) external pure returns (string memory); + function toString(bytes calldata) external pure returns (string memory); + function store(address, bytes32, bytes32) external; + function load(address, bytes32) external view returns (bytes32); + function warp(uint256) external; + function snapshotState() external returns (uint256); + function revertToStateAndDelete(uint256) external returns (bool); + function record() external; + function accesses(address) external returns (bytes32[] memory, bytes32[] memory); +} + +/// Executes the actual creation/runtime bytecode. No contract arithmetic or +/// storage layout logic is duplicated here. All slices in v1 are read-only. +contract SliceDifferentialTest { + VmSlice constant vm = VmSlice(address(uint160(uint256(keccak256("hevm cheat code"))))); + uint256 constant CALL_GAS = 30_000_000; + + function deploy(string memory path) internal returns (address target) { + bytes memory code = vm.readFileBinary(path); + assembly { target := create(0, add(code, 32), mload(code)) } + require(target != address(0) && target.code.length != 0, "harness: deployment failed"); + } + + function number(string memory json, string memory key) internal pure returns (uint256) { + return vm.parseUint(vm.parseJsonString(json, key)); + } + + function runRoute(string memory route, address target, uint256 count) internal { + string memory output = string.concat("out/", route, ".txt"); + vm.writeFile(output, ""); + for (uint256 i; i < count; ++i) { + uint256 snapshot = vm.snapshotState(); + string memory json = vm.readFile(string.concat("evm-cases/", vm.toString(i), ".json")); + string memory prefix = ""; + uint256 entries = vm.parseJsonUint(json, string.concat(prefix, ".storageCount")); + for (uint256 k; k < entries; ++k) { + string memory row = string.concat(prefix, ".storage[", vm.toString(k), "]"); + vm.store(target, bytes32(number(json, string.concat(row, "[0]"))), + bytes32(number(json, string.concat(row, "[1]")))); + } + vm.warp(number(json, string.concat(prefix, ".timestamp"))); + bytes memory data = vm.parseJsonBytes(json, string.concat(prefix, ".", route, "Calldata")); + vm.record(); + uint256 beforeGas = gasleft(); + (bool ok, bytes memory returned) = target.call{gas: CALL_GAS}(data); + uint256 spent = beforeGas - gasleft(); + // Exhausted gas or exceptional halts are resource/harness failures, + // never evidence that the Solidity and model both reverted. + require(spent < CALL_GAS, "harness: execution resource limit"); + (, bytes32[] memory writes) = vm.accesses(target); + require(writes.length == 0, "harness: read-only slice wrote storage"); + string memory slots; + uint256 observed = vm.parseJsonUint(json, string.concat(prefix, ".observeCount")); + for (uint256 k; k < observed; ++k) { + uint256 slot = number(json, string.concat(prefix, ".observe[", vm.toString(k), "]")); + slots = string.concat(slots, " ", vm.toString(uint256(vm.load(target, bytes32(slot))))); + } + vm.writeLine(output, string.concat(vm.toString(i), ok ? " ok " : " revert ", vm.toString(returned), slots)); + require(vm.revertToStateAndDelete(snapshot), "harness: snapshot restore failed"); + } + } + + function test_differential() public { + uint256 count = vm.parseJsonUint(vm.readFile("cases.json"), ".count"); + runRoute("source", deploy("source.bin"), count); + runRoute("compiled", deploy("compiled.bin"), count); + } +} diff --git a/scripts/solidity_differential/__init__.py b/scripts/solidity_differential/__init__.py new file mode 100644 index 0000000000..c2637640b8 --- /dev/null +++ b/scripts/solidity_differential/__init__.py @@ -0,0 +1 @@ +"""Differential execution of covered Solidity imports (test tooling only).""" diff --git a/scripts/solidity_differential/cases.py b/scripts/solidity_differential/cases.py new file mode 100644 index 0000000000..ea3e35d426 --- /dev/null +++ b/scripts/solidity_differential/cases.py @@ -0,0 +1,117 @@ +"""Independent ABI/storage fixture construction from solc metadata.""" +import random + +MODULUS = 1 << 256 + + +def keccak(data): + from eth_hash.auto import keccak as hash_bytes + return hash_bytes(data) + + + +def canonical(abi): + return "(" + ",".join(map(canonical, abi["components"])) + ")" + abi["type"][5:] if abi["type"].startswith("tuple") else abi["type"] + + +def abi_value(abi, path, arguments, noise=b""): + ty = abi["type"] + salt = keccak(noise + path.encode()) if noise else bytes(32) + if ty.endswith("]"): + base, length = ty.rsplit("[", 1) + length = length[:-1] + return [abi_value({**abi, "type": base}, path + f"[{i}]", arguments, noise) for i in range(int(length) if length else salt[0] % 3)] + if ty == "tuple": + return tuple(abi_value(child, path + "." + child["name"], arguments, noise) for child in abi["components"]) + bits = 160 if ty == "address" else (1 if ty == "bool" else int(ty[4:] or 256) if ty.startswith("uint") else int(ty[3:] or 256) if ty.startswith("int") else int(ty[5:]) * 8 if ty.startswith("bytes") and ty != "bytes" else 256) + n = arguments.get(path, int.from_bytes(salt, "big") % (1 << bits)) + if ty.startswith("int") and path not in arguments and n >= 1 << (bits - 1): + n -= 1 << bits + if ty == "address": + return n.to_bytes(20, "big") + if ty.startswith("bytes"): + return n.to_bytes(int(ty[5:]), "big") if ty != "bytes" else salt[:salt[0] % 3] + if ty == "string": + return salt[:salt[0] % 3].hex() + if ty == "bool": + if n not in (0, 1): + raise ValueError("invalid boolean fixture") + return bool(n) + return n + + +def mapping_slot(base, key): + return int.from_bytes(keccak(key.to_bytes(32, "big") + base.to_bytes(32, "big")), "big") + + +def storage_words(layout, recipes, values): + result = {} + for recipe in recipes: + field = next(f for f in layout["storage"] if f["label"] == recipe["field"]) + slot, type_id = int(field["slot"]), field["type"] + for name in recipe["keys"]: + ty = layout["types"][type_id] + if ty["encoding"] != "mapping": + raise ValueError("storage recipe key count exceeds mapping depth") + slot = mapping_slot(slot, values[name]) + type_id = ty["value"] + members = layout["types"][type_id]["members"] + for member, name in recipe["members"].items(): + item = next(m for m in members if m["label"] == member) + ty = layout["types"][item["type"]] + if not ty["label"].startswith("uint"): + raise ValueError("only unsigned packed storage fixtures are supported") + width = int(ty["numberOfBytes"]) * 8 + value = values[name] + if not 0 <= value < 1 << width: + raise ValueError(f"out of range storage value {member}") + address = (slot + int(item["slot"])) % MODULUS + shift = int(item["offset"]) * 8 + mask = ((1 << width) - 1) << shift + # Fill unused packed members with noise, not assumed zeros. + old = result.get(address, int.from_bytes(keccak(address.to_bytes(32, "big")), "big")) + result[address] = (old & ~mask) | (value << shift) + return result + + +def materialize(config, metadata, abi, layout, values, name): + from eth_abi import encode + arguments = {path: values[var] for path, var in config["arguments"].items()} + model_args = [] + projected = {p["modelParam"]: p["parameter"] + "." + p["member"] for p in metadata["projections"]} + for param in metadata["params"]: + model_args.append(arguments[projected.get(param, param)]) + signature = abi["name"] + "(" + ",".join(map(canonical, abi["inputs"])) + ")" + source_data = keccak(signature.encode())[:4] + encode( + [canonical(x) for x in abi["inputs"]], [abi_value(x, x["name"], arguments, name.encode()) for x in abi["inputs"]]) + compiled_data = bytes.fromhex("12345678") + b"".join(n.to_bytes(32, "big") for n in model_args) + storage = storage_words(layout, config.get("storage", []), values) + # Adjacent slots and unrelated words detect offsets / accidental reads. + for slot in list(storage): + for neighbor in ((slot - 1) % MODULUS, (slot + 1) % MODULUS): + storage.setdefault(neighbor, int.from_bytes(keccak(neighbor.to_bytes(32, "big")), "big")) + for slot in range(4): + storage.setdefault(slot, MODULUS - slot - 1) + observed = sorted(storage) + return {"id": name, "args": list(map(str, model_args)), "timestamp": str(values.get(config.get("timestamp", ""), 0)), + "storage": [[str(k), str(v)] for k, v in sorted(storage.items())], "observe": list(map(str, observed)), + "storageCount": len(storage), "observeCount": len(observed), + "sourceCalldata": "0x" + source_data.hex(), "compiledCalldata": "0x" + compiled_data.hex()} + + +def generate(config, count, seed, corpus): + rng = random.Random(seed) + domains = config["variables"] + seeds = [{key: int(row.get(key, config.get("defaults", {}).get(key, 0))) for key in domains} for row in corpus] or [{key: 0 for key in domains}] + result = [(row.get("name", f"corpus-{i}"), values) for i, (row, values) in enumerate(zip(corpus, seeds))] + for i in range(count): + values = dict(seeds[i % len(seeds)]) if i % 3 else {} + for key, bits in domains.items(): + edge = [0, 1, 2, (1 << bits) - 1, (1 << bits) - 2, 1 << (bits - 1)] + if key not in values or rng.randrange(3) == 0: + values[key] = rng.choice(edge) if rng.randrange(2) else rng.getrandbits(bits) + if i % 2 == 0: + for low, high in config.get("ordered_pairs", []): + values[low], values[high] = sorted((values[low], values[high])) + result.append((f"seed-{seed}-{i}", values)) + return result diff --git a/scripts/solidity_differential/engine.py b/scripts/solidity_differential/engine.py new file mode 100644 index 0000000000..b2da922981 --- /dev/null +++ b/scripts/solidity_differential/engine.py @@ -0,0 +1,235 @@ +"""Batch A/B/C execution with explicit infrastructure failures and replay artifacts.""" +import hashlib +import json +from pathlib import Path +import shutil +import subprocess +import time + +from .cases import canonical, generate, materialize + +ROOT = Path(__file__).resolve().parents[2] +WORKSPACE = Path.cwd().resolve() +SOLC = WORKSPACE / ".lake/solidity-import/solc-0.8.34" + + +class HarnessError(RuntimeError): + pass + + +def write_json(path, obj): + path.write_text(json.dumps(obj, indent=2) + "\n") + + +def command(argv, *, cwd=WORKSPACE, timeout=180, stdin=None, log=None): + try: + result = subprocess.run(list(map(str, argv)), cwd=cwd, input=stdin, text=True, capture_output=True, timeout=timeout) + except subprocess.TimeoutExpired as error: + raise HarnessError(f"resource_limit: {argv[0]} exceeded {timeout}s") from error + if log: + Path(log).write_text(result.stdout + result.stderr) + if result.returncode: + raise HarnessError(f"tool_failure ({result.returncode}): {' '.join(map(str, argv))}\n{(result.stdout + result.stderr)[-4000:]}") + return result.stdout + + +def solc_compile(request, project, out): + write_json(out.with_suffix(".input.json"), request) + stdout = command([SOLC, "--base-path", project, "--standard-json"], stdin=json.dumps(request), timeout=180) + result = json.loads(stdout) + write_json(out.with_suffix(".output.json"), result) + errors = [e.get("formattedMessage", e["message"]) for e in result.get("errors", []) if e["severity"] == "error"] + if errors: + raise HarnessError("solc_failure: " + "\n".join(errors)) + return result + + +def implementation_hashes(): + hashes = {str(p.relative_to(ROOT)): hashlib.sha256(p.read_bytes()).hexdigest() + for directory in ("Compiler", "Verity", "scripts/solidity_differential") + for p in (ROOT / directory).rglob("*") if p.suffix in (".lean", ".py", ".sol", ".txt")} + + for name in ("scripts/solidity_import_differential.py", "lake-manifest.json", "lean-toolchain"): + hashes[name] = hashlib.sha256((ROOT / name).read_bytes()).hexdigest() + return hashes + + +def prepare(config_path, output): + config_path = Path(config_path).resolve() + config = json.loads(config_path.read_text()) + project = (config_path.parent / config["project"]).resolve() + output = Path(output).resolve() + output.mkdir(parents=True, exist_ok=True) + (output / "out").mkdir(exist_ok=True) + if not isinstance(config.get("variables"), dict) or not config["variables"]: + raise HarnessError("fixture_unsupported: nonempty variable domains required") + for name, bits in config["variables"].items(): + if not isinstance(bits, int) or not 1 <= bits <= 256: + raise HarnessError("invalid variable width: " + name) + settings = config.get("settings", {}) + if set(settings) - {"evmVersion", "viaIR", "optimizer", "metadata"}: + raise HarnessError("fixture_unsupported: unknown compiler settings") + profile = {"evmVersion": "osaka", "viaIR": True, "optimizer": {"enabled": True, "runs": 466}, + "metadata": {"bytecodeHash": "none"}, **settings} + if profile["evmVersion"] != "osaka": + raise HarnessError("fixture_unsupported: this runner currently targets Osaka") + command(["lake", "build", "Compiler.SolidityImport.Import", "Compiler.SolidityImport.Differential"], + timeout=1800, log=output / "build.log") + # Keep the importer and reference compiler profile identical. + driver = output / "Driver.lean" + runs = f'some {profile["optimizer"]["runs"]}' if profile["optimizer"]["enabled"] else "none" + # solc type strings ("struct Market") become Solidity spellings ("Market"). + written = [ty.split(" ", 1)[1] if ty.split(" ", 1)[0] in ("struct", "contract", "enum") else ty + for ty in config["param_types"]] + driver.write_text('import Compiler.SolidityImport.Import\nimport Compiler.SolidityImport.Differential\n' + + f'solidity_import tested from {json.dumps(str(project))} entry {json.dumps(config["entry"])}\n' + + f' using {{ evmVersion := {json.dumps(profile["evmVersion"])}, viaIR := {str(profile["viaIR"]).lower()}, ' + + f'optimizerRuns := {runs}, bytecodeHash := {json.dumps(profile["metadata"]["bytecodeHash"])} }}\n' + + f' contract {config["contract"]}\n' + + f' function {config["function"]}({", ".join(written)})\n' + + 'def main (args : List String) : IO UInt32 :=\n' + + ' Compiler.CompilationModel.SolidityImport.Differential.run tested.model tested.report args\n') + command(["lake", "env", "lean", "--run", driver, "describe", config["function"], output / "model.json", output / "model.yul"], + timeout=300, log=output / "import.log") + metadata = json.loads((output / "model.json").read_text()) + if not metadata["compilable"]: + raise HarnessError("compilation_unsupported: " + metadata["compileError"]) + imported_settings = json.loads(metadata["settings"]) + source_request = {"language": "Solidity", "sources": {config["entry"]: {"urls": [config["entry"]]}}, + "settings": {**imported_settings, "outputSelection": {"*": {"*": ["abi", "storageLayout", "evm.bytecode.object", "evm.deployedBytecode.object"]}}}} + source_result = solc_compile(source_request, project, output / "source") + contract = source_result["contracts"][config["entry"]][config["contract"]] + constructors = [x for x in contract["abi"] if x["type"] == "constructor"] + if constructors and constructors[0]["inputs"]: + raise HarnessError("fixture_unsupported: constructor arguments require an explicit deployment fixture") + matches = [x for x in contract["abi"] if x["type"] == "function" and x["name"] == config["function"] + and [p.get("internalType", p["type"]).replace(" payable", "") for p in x["inputs"]] == config["param_types"]] + if len(matches) != 1: + raise HarnessError("fixture_unsupported: selected function ABI is missing or ambiguous") + abi = matches[0] + if any(p["type"] in ("bytes", "string") or "[" in p["type"] or p["type"] == "tuple" for p in abi["outputs"]): + raise HarnessError("fixture_unsupported: only static scalar returns are compared") + source_bin = bytes.fromhex(contract["evm"]["bytecode"]["object"]) + (output / "source.bin").write_bytes(source_bin) + yul_request = {"language": "Yul", "sources": {"Model.yul": {"content": (output / "model.yul").read_text()}}, + "settings": {"evmVersion": profile["evmVersion"], "optimizer": profile["optimizer"], + "outputSelection": {"*": {"*": ["evm.bytecode.object", "evm.deployedBytecode.object"]}}}} + compiled_result = solc_compile(yul_request, project, output / "compiled") + compiled_contracts = list(compiled_result["contracts"]["Model.yul"].values()) + if len(compiled_contracts) != 1: + raise HarnessError("harness: expected one compiled Yul object") + (output / "compiled.bin").write_bytes(bytes.fromhex(compiled_contracts[0]["evm"]["bytecode"]["object"])) + shutil.copyfile(Path(__file__).with_name("Runner.t.sol"), output / "Runner.t.sol") + (output / "foundry.toml").write_text('[profile.default]\nsrc = "empty"\ntest = "."\n' + + f'solc = {json.dumps(str(SOLC))}\nevm_version = "osaka"\noptimizer = true\nvia_ir = true\n' + + 'gas_limit = 9223372036854775807\ncode_size_limit = 100000\n' + + 'fs_permissions = [{ access = "read-write", path = "./" }]\n') + manifest = {"config": config, "configPath": str(config_path), "model": metadata, + "verity": command(["git", "rev-parse", "HEAD"], cwd=ROOT).strip(), + "solcSha256": hashlib.sha256(SOLC.read_bytes()).hexdigest(), + "forge": command(["forge", "--version"]).strip(), + "observables": ["success/revert", "return words", "observed storage", "no storage writes", "A/C revert payload"], + "exclusions": ["Denote revert payload", "gas equivalence", "memory allocation identity", "full ABI equivalence"]} + # Archive the source closure loaded by solc. Reject paths escaping the project. + for logical in source_result["sources"]: + path = (project / logical).resolve() + if not path.is_relative_to(project): + raise HarnessError("source outside project: " + logical) + target = output / "sources" / logical + target.parent.mkdir(parents=True, exist_ok=True) + shutil.copyfile(path, target) + manifest["sourceHashes"] = {str(p.relative_to(output / "sources")): hashlib.sha256(p.read_bytes()).hexdigest() + for p in (output / "sources").rglob("*.sol")} + write_json(output / "abi.json", abi) + write_json(output / "layout.json", contract["storageLayout"]) + manifest["implementationHashes"] = implementation_hashes() + manifest["artifactHashes"] = {name: hashlib.sha256((output / name).read_bytes()).hexdigest() + for name in ("source.bin", "compiled.bin", "model.yul", "Driver.lean", "Runner.t.sol", "foundry.toml", "abi.json", "layout.json") + if (output / name).exists()} + write_json(output / "manifest.json", manifest) + return config, metadata, abi, contract["storageLayout"], driver + + +def parse_evm(path, cases): + lines = path.read_text().splitlines() + if len(lines) != len(cases): + raise HarnessError("missing EVM results") + results = [] + for i, (line, case) in enumerate(zip(lines, cases)): + index, status, data, *storage = line.split() + if index != str(i) or status not in ("ok", "revert") or len(storage) != len(case["observe"]): + raise HarnessError("invalid EVM result row") + raw = bytes.fromhex(data.removeprefix("0x")) + if status == "ok" and len(raw) % 32: + raise HarnessError("invalid scalar ABI result length") + results.append({"id": case["id"], "status": status, "data": data, + "words": [str(int.from_bytes(raw[k:k+32], "big")) for k in range(0, len(raw), 32)] if status == "ok" else [], + "storage": storage}) + return results + + +def execute(output, cases): + output = Path(output).resolve() + manifest = json.loads((output / "manifest.json").read_text()) + if implementation_hashes() != manifest["implementationHashes"]: + raise HarnessError("implementation changed; prepare a new campaign") + if not cases: + raise HarnessError("empty campaign is not a passing comparison") + for name, digest in manifest.get("artifactHashes", {}).items(): + if hashlib.sha256((output / name).read_bytes()).hexdigest() != digest: + raise HarnessError("campaign artifact changed: " + name) + if hashlib.sha256(SOLC.read_bytes()).hexdigest() != manifest["solcSha256"]: + raise HarnessError("compiler binary changed") + if command(["forge", "--version"]).strip() != manifest["forge"]: + raise HarnessError("Foundry version changed") + if len({case["id"] for case in cases}) != len(cases): + raise HarnessError("duplicate case identities") + write_json(output / "model-cases.json", cases) + write_json(output / "cases.json", {"count": len(cases)}) + # Parse one small document per EVM case, avoiding quadratic JSON work. + (output / "evm-cases").mkdir(exist_ok=True) + for index, case in enumerate(cases): + write_json(output / "evm-cases" / f"{index}.json", case) + # Delete old outputs: a broken process cannot pass by leaving stale results. + for file in ("model-results.json", "out/source.txt", "out/compiled.txt"): + (output / file).unlink(missing_ok=True) + command(["lake", "env", "lean", "--run", output / "Driver.lean", "run", manifest["config"]["function"], output / "model-cases.json", output / "model-results.json", manifest["model"]["digest"]], + timeout=300, log=output / "model-run.log") + command(["forge", "test", "--root", output, "--match-contract", "SliceDifferentialTest", "-vv"], + cwd=output, timeout=300, log=output / "forge.log") + source = parse_evm(output / "out/source.txt", cases) + compiled = parse_evm(output / "out/compiled.txt", cases) + model = json.loads((output / "model-results.json").read_text()) + if len(model) != len(cases): + raise HarnessError("missing model results") + divergences = [] + for case, a, b, c in zip(cases, source, model, compiled): + if b["status"] not in ("ok", "revert"): + raise HarnessError("invalid model status") + if b["id"] != case["id"]: + raise HarnessError("model result identity mismatch") + observable = lambda r: (r["status"], r["words"], r["storage"]) + if observable(a) != observable(b) or observable(b) != observable(c) or a["data"] != c["data"]: + divergences.append({"case": case, "source": a, "model": b, "compiled": c}) + report = {"cases": len(cases), "successes": sum(r["status"] == "ok" for r in source), + "reverts": sum(r["status"] == "revert" for r in source), "divergences": divergences} + write_json(output / "results.json", report) + return report + + +def campaign(config_path, output, count, seed): + started = time.monotonic() + config, metadata, abi, layout, _ = prepare(config_path, output) + corpus_path = config.get("corpus") + corpus = json.loads((Path(config_path).resolve().parent / corpus_path).read_text()) if corpus_path else [] + values = generate(config, count, seed, corpus) + write_json(Path(output) / "values.json", [{"name": name, **{k: str(v) for k, v in row.items()}} for name, row in values]) + cases = [materialize(config, metadata, abi, layout, row, name) for name, row in values] + manifest_path = Path(output) / "manifest.json" + manifest = json.loads(manifest_path.read_text()) + manifest["casesSha256"] = hashlib.sha256(json.dumps(cases, sort_keys=True).encode()).hexdigest() + write_json(manifest_path, manifest) + report = execute(output, cases) + report.update(seed=seed, seconds=round(time.monotonic() - started, 3)) + write_json(Path(output) / "results.json", report) + return report diff --git a/scripts/solidity_differential/mutations.py b/scripts/solidity_differential/mutations.py new file mode 100644 index 0000000000..0443456c44 --- /dev/null +++ b/scripts/solidity_differential/mutations.py @@ -0,0 +1,73 @@ +"""Compile actual importer/Denote mutants in isolated source/cache snapshots.""" +import json +from pathlib import Path +import shutil +import subprocess +import sys + +from .engine import ROOT, WORKSPACE, HarnessError, command, write_json + +MUTANTS = { + "import-comparison": ("Compiler/SolidityImport/Import.lean", '| "<" => cmp .lt left right', '| "<" => cmp .gt left right'), + "import-field-slot": ("Compiler/SolidityImport/Import.lean", 'slot := some slot }', 'slot := some (slot + 1) }'), + "denote-packed-mask": ("Verity/Core/Model/Denote.lean", '(2 ^ packed.width) - 1', '(2 ^ packed.width) - 2'), + "denote-storage-read": ("Verity/Core/Model/Denote.lean", ' world.readSlot (wordNormalize slot)\n', ' world.readSlot (wordNormalize (slot + 1))\n'), +} + + +def snapshot(destination): + destination.mkdir(parents=True, exist_ok=False) + paths = command(["git", "ls-files", "--cached", "--others", "--exclude-standard", "-z"], cwd=ROOT).split("\0") + for name in paths: + if not name or not (ROOT / name).is_file(): + continue + target = destination / name + target.parent.mkdir(parents=True, exist_ok=True) + shutil.copyfile(ROOT / name, target) + cache = destination / ".lake" + cache.mkdir(exist_ok=True) + (cache / "packages").symlink_to(WORKSPACE / ".lake/packages", target_is_directory=True) + # Private copy-on-write cache: no hardlinks/symlinks to mutable parent oleans. + if sys.platform == "darwin": + command(["cp", "-cR", WORKSPACE / ".lake/build", cache / "build"], timeout=300) + else: + command(["cp", "-a", "--reflink=auto", WORKSPACE / ".lake/build", cache / "build"], timeout=300) + (cache / "solidity-import").mkdir() + shutil.copy2(WORKSPACE / ".lake/solidity-import/solc-0.8.34", cache / "solidity-import/solc-0.8.34") + + +def mutation_campaign(output, selected=None): + output = Path(output).resolve() + output.mkdir(parents=True, exist_ok=True) + reports = [] + for name in selected or MUTANTS: + relative, before, after = MUTANTS[name] + directory = output / name + if directory.exists(): + raise HarnessError("mutation output already exists; choose a fresh output: " + str(directory)) + snapshot(directory) + source = directory / relative + text = source.read_text() + if text.count(before) != 1: + raise HarnessError("mutation anchor drifted: " + name) + source.write_text(text.replace(before, after)) + argv = [sys.executable, str(directory / "scripts/solidity_import_differential.py"), + "--config", str(directory / "Contracts/SolidityImportSmoke/differential.json"), + "--output", str(directory / ".lake/campaign"), "--cases", "8"] + try: + result = subprocess.run(argv, cwd=directory, text=True, capture_output=True, timeout=600) + (directory / "mutation.log").write_text(result.stdout + result.stderr) + report_file = directory / ".lake/campaign/results.json" + report = json.loads(report_file.read_text()) if report_file.exists() else {} + if result.returncode == 1 and report.get("divergences"): + status = "detected" + elif result.returncode == 0: + status = "survived" + else: + status = "invalid" # compilation/tool failure does not kill a mutant + except subprocess.TimeoutExpired: + status = "invalid" + reports.append({"mutant": name, "status": status, "path": relative, "output": str(directory)}) + write_json(output / "mutation-results.json", reports) + print(f"{name}: {status}", flush=True) + return {"mutants": reports, "divergences": [r for r in reports if r["status"] != "detected"]} diff --git a/scripts/solidity_differential/programs.py b/scripts/solidity_differential/programs.py new file mode 100644 index 0000000000..0072b729c2 --- /dev/null +++ b/scripts/solidity_differential/programs.py @@ -0,0 +1,115 @@ +"""Small typed source programs; solc/EVM supplies the expected behavior.""" +import copy +import json +from pathlib import Path +import random + +from .engine import campaign, write_json + + +def expression(rng, depth, bits): + if depth == 0 or rng.randrange(4) == 0: + return {"kind": rng.choice(["x", "y", "literal"]), "value": rng.choice([0, 1, 2, (1 << bits) - 1])} + return {"kind": "binary", "op": rng.choice(["+", "-", "*", "/"]), + "left": expression(rng, depth - 1, bits), "right": expression(rng, depth - 1, bits)} + + +def render_expr(node, bits, x="x", y="y"): + ty = f"uint{bits}" + if node["kind"] == "binary": + return f'({render_expr(node["left"], bits, x, y)} {node["op"]} {render_expr(node["right"], bits, x, y)})' + if node["kind"] == "literal": + return f'{ty}({node["value"]})' + return f'{ty}({x if node["kind"] == "x" else y})' + + +def source(spec): + bits = spec["bits"] + x = "_verity_slice_tmp_0" if spec["renamed"] else "x" + member = "tmp_0" if spec.get("projection_collision") else "maturity" + market = "_verity_slice" if spec.get("projection_collision") else "m" + expr = render_expr(spec["expression"], bits, x) + helper = "" + if spec["helper"]: + helper = f'''library L {{ + function work(uint{bits} x, uint{bits} y) internal pure returns (uint{bits}) {{ + return {render_expr(spec['expression'], bits)}; + }} +}}''' + expr = f"L.work(uint{bits}({x}), uint{bits}(y))" + return f'''// SPDX-License-Identifier: MIT +pragma solidity 0.8.34; +struct Mkt {{ uint256 ignored; uint128[] ignoredArray; uint256 {member}; }} +{helper} +contract C {{ + function f(Mkt memory {market}, uint256 {x}, uint256 y) external pure returns (uint256, uint256, uint256) {{ + uint256 stamp = {market}.{member}; + uint{bits} a = {expr}; + uint{bits} b = stamp < {x} ? a : uint{bits}(y); + return (uint256(a), uint256(b), stamp); + }} +}} +''' + + +def write_program(directory, spec): + directory = Path(directory) + directory.mkdir(parents=True, exist_ok=True) + (directory / "Slice.sol").write_text(source(spec)) + write_json(directory / "program.json", spec) + config = {"project": ".", "entry": "Slice.sol", "contract": "C", "function": "f", + "param_types": ["struct Mkt", "uint256", "uint256"], + "variables": {"x": 256, "y": 256, "maturity": 256}, + "arguments": {"_verity_slice.tmp_0" if spec.get("projection_collision") else "m.maturity": "maturity", "_verity_slice_tmp_0" if spec["renamed"] else "x": "x", "y": "y"}, + "storage": [], "corpus": "corpus.json"} + write_json(directory / "fixture.json", config) + write_json(directory / "corpus.json", [{"name": "zero", "x": "0", "y": "0", "maturity": "0"}, + {"name": "small", "x": "3", "y": "2", "maturity": "7"}, + {"name": "wrap-product", "x": str(1 << 128), "y": str(1 << 128), "maturity": "1"}]) + return directory / "fixture.json" + + +def smaller_expressions(node): + if node["kind"] == "binary": + yield node["left"] + yield node["right"] + for field in ("left", "right"): + for replacement in smaller_expressions(node[field]): + result = copy.deepcopy(node) + result[field] = replacement + yield result + if node != {"kind": "literal", "value": 0}: + yield {"kind": "literal", "value": 0} + + +def generated_campaign(output, count, cases, seed): + output = Path(output).resolve() + output.mkdir(parents=True, exist_ok=True) + rng = random.Random(seed) + reports = [] + for i in range(count): + bits = [8, 16, 128, 248, 256][i % 5] + tree = expression(rng, 2, bits) + # First five explicitly exercise each checked width, especially uint248. + if i < 5: + tree = {"kind": "binary", "op": "*", "left": {"kind": "x"}, "right": {"kind": "y"}} + reference = None + for variant in (0, 1, 2): + spec = {"bits": bits, "expression": tree, "renamed": variant == 1, "helper": variant != 0, "projection_collision": variant == 2} + directory = output / f"program-{i}-{variant}" + fixture = write_program(directory, spec) + report = campaign(fixture, directory / "run", cases, seed + i) + reports.append({"program": i, "variant": variant, **{k: v for k, v in report.items() if k != "divergences"}}) + write_json(output / "program-results.json", reports) + if report["divergences"]: + return {"programs": len(reports), "divergences": report["divergences"]} + # Renaming + helper extraction must preserve the observed behavior. + rows = (directory / "run/out/source.txt").read_text() + if reference is not None and reference != rows: + divergence = {"metamorphic": str(directory), "reference": str(reference_dir), + "rows": [{"line": k, "reference": a, "variant": b} for k, (a, b) + in enumerate(zip(reference.splitlines(), rows.splitlines())) if a != b]} + write_json(output / "metamorphic-divergence.json", divergence) + return {"programs": len(reports), "divergences": [divergence]} + reference, reference_dir = rows, directory + return {"programs": len(reports), "cases": sum(r["cases"] for r in reports), "divergences": []} diff --git a/scripts/solidity_differential/reduce.py b/scripts/solidity_differential/reduce.py new file mode 100644 index 0000000000..7f4df81590 --- /dev/null +++ b/scripts/solidity_differential/reduce.py @@ -0,0 +1,82 @@ +"""Budgeted reducers which preserve the original observable mismatch category.""" +import copy +import hashlib +import json +from pathlib import Path +import shutil +import time + +from .cases import materialize +from .engine import campaign, execute, write_json +from .programs import smaller_expressions, write_program + + +def signature(divergence): + if "metamorphic" in divergence: + return ("metamorphic",) + routes = [divergence[k] for k in ("source", "model", "compiled")] + return tuple((key, routes[0][key] == routes[1][key], routes[1][key] == routes[2][key]) + for key in ("status", "words", "storage")) + (("revert-data", routes[0]["data"] == routes[2]["data"]),) + + +def reduce_failure(directory, seconds=120): + directory = Path(directory).resolve() + report = json.loads((directory / "results.json").read_text()) + if not report["divergences"]: + raise ValueError("campaign has no divergence to reduce") + failure = report["divergences"][0] + if "case" not in failure: + raise ValueError("only A/B/C case divergences are reducible; inspect metamorphic-divergence.json") + target = signature(failure) + manifest = json.loads((directory / "manifest.json").read_text()) + config, metadata = manifest["config"], manifest["model"] + abi = json.loads((directory / "abi.json").read_text()) + layout = json.loads((directory / "layout.json").read_text()) + name = failure["case"]["id"] + row = next(row for row in json.loads((directory / "values.json").read_text()) if row["name"] == name) + best = {k: int(row[k]) for k in config["variables"]} + scratch = directory / "reduced" + scratch.mkdir(exist_ok=True) + for path in directory.iterdir(): + if path.is_file() and path.name not in ("results.json", "values.json"): + shutil.copyfile(path, scratch / path.name) + (scratch / "out").mkdir(exist_ok=True) + deadline = time.monotonic() + seconds + attempts = 0 + for key in best: + candidates = sorted({0, 1, 2, best[key] // 2, best[key] & (best[key] - 1)}) + for value in candidates: + if value >= best[key] or time.monotonic() >= deadline: + continue + trial = {**best, key: value} + result = execute(scratch, [materialize(config, metadata, abi, layout, trial, name)]) + attempts += 1 + if result["divergences"] and signature(result["divergences"][0]) == target: + best = trial + final = execute(scratch, [materialize(config, metadata, abi, layout, best, name)]) + if not final["divergences"] or signature(final["divergences"][0]) != target: + raise ValueError("reduced input did not preserve the divergence") + write_json(scratch / "values.json", [{"name": name, **{k: str(v) for k, v in best.items()}}]) + reduced_cases = json.loads((scratch / "model-cases.json").read_text()) + manifest["casesSha256"] = hashlib.sha256(json.dumps(reduced_cases, sort_keys=True).encode()).hexdigest() + write_json(scratch / "manifest.json", manifest) + # Generated programs additionally admit a typed AST reduction. Do not edit + # arbitrary production Solidity by textual deletion. + program_path = directory.parent / "program.json" + if program_path.exists(): + spec = json.loads(program_path.read_text()) + for tree in smaller_expressions(spec["expression"]): + if time.monotonic() >= deadline: + break + trial = copy.deepcopy(spec) + trial["expression"] = tree + program_dir = scratch / "program" + fixture = write_program(program_dir, trial) + write_json(program_dir / "corpus.json", [{"name": name, **{k: str(v) for k, v in best.items()}}]) + result = campaign(fixture, program_dir / "run", 0, 0) + attempts += 1 + if result["divergences"] and signature(result["divergences"][0]) == target: + spec = trial + write_json(scratch / "minimal-program.json", spec) + (scratch / "minimal.sol").write_text((program_dir / "Slice.sol").read_text()) + return {"attempts": attempts, "output": str(scratch), "divergences": final["divergences"]} diff --git a/scripts/solidity_differential/requirements.txt b/scripts/solidity_differential/requirements.txt new file mode 100644 index 0000000000..2387783ce0 --- /dev/null +++ b/scripts/solidity_differential/requirements.txt @@ -0,0 +1,2 @@ +eth-abi==5.2.0 +eth-hash[pycryptodome]==0.7.1 diff --git a/scripts/solidity_import_differential.py b/scripts/solidity_import_differential.py new file mode 100755 index 0000000000..d05d3bbf14 --- /dev/null +++ b/scripts/solidity_import_differential.py @@ -0,0 +1,63 @@ +#!/usr/bin/env python3 +"""Run reproducible A/B/C comparisons for a declarative Solidity slice fixture.""" +import argparse +import hashlib +import json +from pathlib import Path +import sys +from solidity_differential.engine import HarnessError, campaign, execute + + +def main(): + parser = argparse.ArgumentParser(description=__doc__) + parser.add_argument("--config", type=Path) + parser.add_argument("--output", type=Path, required=True) + parser.add_argument("--seed", type=int, default=2438) + parser.add_argument("--cases", type=int, default=128, help="generated cases in addition to the fixed corpus") + parser.add_argument("--replay", action="store_true", help="replay materialized cases in --output") + parser.add_argument("--programs", type=int, default=0, help="generate this many source programs, each with a renamed/helper variant") + parser.add_argument("--reduce", action="store_true", help="reduce the first recorded divergence") + parser.add_argument("--reduce-seconds", type=int, default=120) + parser.add_argument("--mutations", action="store_true", help="test importer and Denote mutants in isolated snapshots") + parser.add_argument("--mutant", action="append", help="select a named mutant") + args = parser.parse_args() + if args.cases < 0 or args.programs < 0 or args.reduce_seconds <= 0: + parser.error("case/program counts must be nonnegative and reduction budget positive") + if sum(map(bool, (args.config, args.replay, args.programs, args.reduce, args.mutations))) != 1: + parser.error("choose exactly one of --config, --replay, --programs, --reduce, --mutations") + if args.mutant and not args.mutations: + parser.error("--mutant requires --mutations") + try: + if args.mutations: + from solidity_differential.mutations import mutation_campaign + report = mutation_campaign(args.output, args.mutant) + elif args.reduce: + from solidity_differential.reduce import reduce_failure + report = reduce_failure(args.output, args.reduce_seconds) + elif args.programs: + from solidity_differential.programs import generated_campaign + report = generated_campaign(args.output, args.programs, args.cases, args.seed) + elif args.replay: + cases = json.loads((args.output / "model-cases.json").read_text()) + manifest = json.loads((args.output / "manifest.json").read_text()) + if hashlib.sha256(json.dumps(cases, sort_keys=True).encode()).hexdigest() != manifest["casesSha256"]: + raise HarnessError("replay cases changed") + report = execute(args.output, cases) + else: + if not args.config: + parser.error("--config is required unless replaying") + report = campaign(args.config, args.output.resolve(), args.cases, args.seed) + except (HarnessError, ValueError, KeyError, OSError) as error: + print(f"HARNESS ERROR: {error}", file=sys.stderr) + return 2 + print(json.dumps({k: v for k, v in report.items() if k != "divergences"})) + if args.reduce: + return 0 # success means the original divergence was preserved + if report["divergences"]: + print(f"DIVERGENCE: inspect the reports in {args.output}", file=sys.stderr) + return 1 + return 0 + + +if __name__ == "__main__": + sys.exit(main()) diff --git a/scripts/solidity_import_mutations.py b/scripts/solidity_import_mutations.py new file mode 100644 index 0000000000..5f9b44ba07 --- /dev/null +++ b/scripts/solidity_import_mutations.py @@ -0,0 +1,389 @@ +#!/usr/bin/env python3 +"""Mutation checks for the Solidity importer. + +Each case copies the smoke project, changes one thing, and elaborates +`solidity_import`. A reached helper, a return-order change, and a layout +change must make the witness check fail. An unreached `for` stays importable. +Reaching that `for` must be rejected with a source position, and the failed +process must not leave an olean. +""" + +import shutil +import subprocess +import sys +from pathlib import Path + +ROOT = Path(__file__).resolve().parents[1] +SOLC = ROOT / ".lake/solidity-import/solc-0.8.34" +WORK = ROOT / ".lake/import-mutations" +SMOKE = ROOT / "Contracts/SolidityImportSmoke" +GOLDEN = SMOKE / "model.golden" + +HEADER = """ +import Compiler.SolidityImport.Import +import Compiler.SolidityImport.Coverage + +open Compiler.CompilationModel +open Compiler.CompilationModel.SolidityImport +open Compiler.CompilationModel.Denote + +solidity_import imported from "{root}" entry "Slice.sol" + using {{ evmVersion := "osaka", viaIR := true, optimizerRuns := some 466, bytecodeHash := "none" }} + contract C + function f(Mkt, bytes32, address) +""" + +WITNESS = r""" +def sliceOracle : DenoteOracle where + mappingSlot base key := base + key + 1 + keccakMemorySlice _ _ _ := 0 + +def packHalves (lo hi : Nat) : Nat := lo + hi * 2 ^ 128 + +def witnessWorld (credit pending lossFactor lastLoss lastAccrual : Nat) : Verity.ContractState := + Verity.defaultState.withStorageWords fun key => + let word := + match key with + | .slot 5 => packHalves credit pending + | .slot 6 => packHalves lastLoss lastAccrual + | .slot 3 => packHalves 0 lossFactor + | _ => 0 + Verity.Core.Uint256.ofNat word + +def witnessBindings (maturity : Nat) : Env := + [("m_maturity", maturity), ("id", 1), ("user", 2)] + +def body : List Stmt := + match imported.model.functions with + | fn :: _ => fn.body + | [] => [] + +#eval IO.println imported.report.sourceDigest + +#eval show IO Unit from do + let success := denoteScalarBody sliceOracle imported.model.fields + (witnessWorld 100 10 0 0 0) 50 (witnessBindings 100) body + unless success == some [95, 5, 5] do + throw (IO.userError s!"witness changed: {success}") + let reverted := denoteScalarBody sliceOracle imported.model.fields + (witnessWorld 1 2 0 0 0) 1 (witnessBindings 1) body + unless reverted == none do + throw (IO.userError s!"revert witness changed: {reverted}") + IO.println imported.report.sourceDigest +""" + + +def lean(path: Path) -> subprocess.CompletedProcess[str]: + return subprocess.run( + ["lake", "env", "lean", str(path)], + cwd=ROOT, + text=True, + capture_output=True, + ) + + +def write_project(name: str, mutate) -> Path: + dest = WORK / name + if dest.exists(): + shutil.rmtree(dest) + dest.mkdir(parents=True) + shutil.copy(SMOKE / "Slice.sol", dest / "Slice.sol") + shutil.copy(SMOKE / "Lib.sol", dest / "Lib.sol") + mutate(dest) + return dest + + +def expect_success(name: str, project: Path, witness: bool, extra: str = "") -> str: + lean_path = project / "Check.lean" + text = HEADER.format(root=project) + if witness: + text += WITNESS + lean_path.write_text(text + extra) + result = lean(lean_path) + if result.returncode != 0: + sys.stderr.write(result.stdout) + sys.stderr.write(result.stderr) + raise SystemExit(f"{name}: expected success") + print(f"pass {name}") + return result.stdout + + +def expect_failure(name: str, project: Path, needle: str) -> str: + lean_path = project / "Check.lean" + olean = project / "Check.olean" + if olean.exists(): + olean.unlink() + lean_path.write_text(HEADER.format(root=project) + WITNESS) + result = subprocess.run( + ["lake", "env", "lean", str(lean_path), "-o", str(olean)], + cwd=ROOT, + text=True, + capture_output=True, + ) + blob = result.stdout + result.stderr + if result.returncode == 0 or needle not in blob or olean.exists(): + sys.stderr.write(blob) + raise SystemExit(f"{name}: expected rejection containing {needle!r}") + print(f"pass {name}") + return blob + + +def unchanged(dest: Path) -> None: + return None + + +def call_unused_from_loss(dest: Path) -> None: + text = (dest / "Slice.sol").read_text() + old = "return marketState[market].lossFactor;" + assert old in text + (dest / "Slice.sol").write_text(text.replace(old, "uint256 ignored = L.unused();\n " + old)) + + +def call_unused(dest: Path) -> None: + text = (dest / "Slice.sol").read_text() + text = text.replace( + "uint128 credit = p.credit;", + "uint256 ignored = L.unused();\n uint128 credit = p.credit;", + ) + (dest / "Slice.sol").write_text(text) + + +def swap_returns(dest: Path) -> None: + text = (dest / "Slice.sol").read_text() + text = text.replace( + "return (uint128(post) - fee, uint128(postFee) - fee, fee);", + "return (fee, uint128(postFee) - fee, uint128(post) - fee);", + ) + (dest / "Slice.sol").write_text(text) + + +def change_rounding(dest: Path) -> None: + # Witness A divides evenly, so floor versus ceil is invisible there. + # An extra division in the reached helper changes that witness. + text = (dest / "Lib.sol").read_text() + text = text.replace("return (x * y) / d;", "return (x * y) / d / 2;") + (dest / "Lib.sol").write_text(text) + + +def change_factor(dest: Path) -> None: + text = (dest / "Slice.sol").read_text() + text = text.replace( + "postFee.mulDivDown(end - lastAccrual, m.maturity - lastAccrual)", + "postFee.mulDivDown(end - lastAccrual + 50, m.maturity - lastAccrual)", + ) + (dest / "Slice.sol").write_text(text) + + +def change_field(dest: Path) -> None: + text = (dest / "Slice.sol").read_text() + text = text.replace("uint128 credit = p.credit;", "uint128 credit = p.pendingFee;") + (dest / "Slice.sol").write_text(text) + + +def swap_layout(dest: Path) -> None: + text = (dest / "Slice.sol").read_text() + text = text.replace( + "uint128 credit;\n uint128 pendingFee;", + "uint128 pendingFee;\n uint128 credit;", + ) + (dest / "Slice.sol").write_text(text) + + +def helper_local_collision(dest: Path) -> None: + # The caller also has `credit`. Inlining must not overwrite that binding. + path = dest / "Lib.sol" + path.write_text(path.read_text().replace( + "return (x * y) / d;", "uint256 credit = x * y; return credit / d;")) + + +def generated_name_collision(dest: Path) -> None: + path = dest / "Slice.sol" + path.write_text(path.read_text().replace("credit = p.credit", "_verity_slice_tmp_0 = p.credit") + .replace("credit.mulDivDown", "_verity_slice_tmp_0.mulDivDown") + .replace("credit > 0", "_verity_slice_tmp_0 > 0") + .replace("credit - post", "_verity_slice_tmp_0 - post") + .replace(", credit)", ", _verity_slice_tmp_0)")) + + +def unrelated_layout(dest: Path) -> None: + path = dest / "Slice.sol" + path.write_text(path.read_text().replace( + "contract C {", "struct Unused { int128 signedValue; }\ncontract C {").replace( + "public marketState;", "public marketState;\n mapping(bytes32 => Unused) private unusedState;")) + + +def named_arguments(dest: Path) -> None: + path = dest / "Lib.sol" + path.write_text(path.read_text().replace( + "return (x * y) / d;", "return named({d: d, y: y, x: x});")) + with path.open("a") as out: + out.write("\nfunction named(uint256 x, uint256 y, uint256 d) pure returns (uint256) { return x*y/d; }\n") + + +def implicit_return(dest: Path) -> None: + path = dest / "Slice.sol" + path.write_text(path.read_text().replace( + "returns (uint128, uint128, uint128)", "returns (uint128 a, uint128 b, uint128 c)").replace( + "return (uint128(post) - fee, uint128(postFee) - fee, fee);", "")) + + +def projected_name_collision(dest: Path) -> None: + path = dest / "Slice.sol" + path.write_text(path.read_text().replace("bytes32 id,", "bytes32 m_maturity,") + .replace("[id]", "[m_maturity]")) + + +def shadow_builtin(dest: Path) -> None: + path = dest / "Slice.sol" + path.write_text(path.read_text().replace("uint256 ignored;", "uint256 timestamp;") + .replace("Mkt memory m,", "Mkt memory block,") + .replace("m.maturity", "block.maturity")) + + +def narrow_multiply(dest: Path) -> None: + (dest / "Slice.sol").write_text("""pragma solidity 0.8.34; +struct Mkt { uint256 maturity; } +contract C { + function f(Mkt memory m, bytes32 id, address user) external pure returns (uint128, uint128, uint128) { + uint248 x = uint248(m.maturity); + uint248 product = x * x; + return (uint128(product), 0, 0); + } +} +""") + + +def digest_of(blob: str) -> str: + lines = [line.strip() for line in blob.splitlines() if len(line.strip()) == 64] + if not lines: + raise SystemExit(f"no digest in output:\n{blob}") + return lines[-1] + + +def main() -> None: + if not SOLC.is_file(): + raise SystemExit(f"missing {SOLC}") + # The checks import the status driver, which `lake build SolidityImportSmoke` does not build. + subprocess.run(["lake", "build", "Compiler.SolidityImport.Differential"], cwd=ROOT, check=True) + WORK.mkdir(parents=True, exist_ok=True) + snapshot = WORK / "model.txt" + snapshot.unlink(missing_ok=True) + base = expect_success("unreached-for", write_project("base", unchanged), witness=True, + extra=f'\n#eval IO.FS.writeFile "{snapshot}" (toString (repr imported.model) ++ "\\n")\n') + # Refactors of the importer must reproduce the imported model exactly. + if "--update-golden" in sys.argv: + shutil.copy(snapshot, GOLDEN) + elif snapshot.read_text() != GOLDEN.read_text(): + raise SystemExit(f"imported model differs from {GOLDEN}; review, then rerun with --update-golden") + print("pass golden-model") + expect_success("helper-local-hygiene", write_project("helper-local", helper_local_collision), witness=True) + expect_success("generated-name-hygiene", write_project("generated-name", generated_name_collision), witness=True) + expect_success("unrelated-layout", write_project("unrelated-layout", unrelated_layout), witness=True) + expect_failure("named-arguments", write_project("named-arguments", named_arguments), "named call arguments") + expect_failure("implicit-return", write_project("implicit-return", implicit_return), "explicit root return") + expect_failure("projection-name-collision", write_project("projection-name", projected_name_collision), "projected parameter name collision") + namespaced = WORK / "base/Namespaced.lean" + namespaced.write_text(HEADER.format(root=WORK / "base").replace( + "solidity_import imported", "namespace Nested\nsolidity_import imported") + + "\nend Nested\nexample : modelImportCovered Nested.imported.model = true := Nested.imported.covered\n") + result = lean(namespaced) + if result.returncode: + raise SystemExit(result.stdout + result.stderr) + print("pass namespaced-import") + expect_success("shadowed-block", write_project("shadowed-block", shadow_builtin), witness=False, + extra='\n#eval show IO Unit from do\n unless imported.report.projections.any (fun p => p.member == "timestamp") do\n throw (IO.userError "shadowed block was treated as a builtin")\n') + expect_success("narrow-multiply-overflow", write_project("narrow-multiply", narrow_multiply), witness=False, + extra=r''' +#eval show IO Unit from do + let oracle : DenoteOracle := { mappingSlot := fun _ _ => 0, keccakMemorySlice := fun _ _ _ => 0 } + let body := match imported.model.functions with | f :: _ => f.body | [] => [] + let run := fun maturity => denoteScalarBody oracle imported.model.fields Verity.defaultState 0 [("m_maturity", maturity)] body + unless run 3 == some [9, 0, 0] do throw (IO.userError "small uint248 product changed") + unless run (2^128) == none do throw (IO.userError "uint248 product overflow was accepted") +''') + two_roots = WORK / "base/TwoRoots.lean" + two_roots.write_text("import Compiler.SolidityImport.Differential\n" + HEADER.format(root=WORK / "base") + WITNESS + f''' +solidity_import both from "{WORK / "base"}" entry "Slice.sol" + using {{ evmVersion := "osaka", viaIR := true, optimizerRuns := some 466, bytecodeHash := "none" }} + contract C + function f(Mkt, bytes32, address) + function lossOf(bytes32) + +#eval show IO Unit from do + -- A second root must not change how the first one is lowered. + let single := imported.model.functions + match both.model.functions with + | [f, loss] => + unless toString (repr [f]) == toString (repr single) do + throw (IO.userError "adding a root changed the first function") + unless loss.name == "lossOf" && loss.params.map (·.name) == ["market"] do + throw (IO.userError s!"unexpected second root {{loss.name}}") + | _ => throw (IO.userError "expected two imported functions") + unless both.model.fields.map (·.name) == ["position", "marketState"] do + throw (IO.userError "roots do not share one field list") + unless both.report.roots == ["f", "lossOf"] do + throw (IO.userError "report roots changed") + -- One status per root. Compiler-proof coverage is never claimed. + unless both.report.functions.map (·.function) == ["f", "lossOf"] && + both.report.functions.all (·.denoteCovered) && + both.report.functions.all (·.compilerProof == CompilerProofStatus.unavailable noCompilerProofReason) do + throw (IO.userError s!"unexpected function statuses {{repr both.report.functions}}") + let status := Differential.statusText both.model both.report + IO.println status + unless (status.splitOn "compilable true").length == 3 && + (status.splitOn "compilerProofCovered unavailable").length == 3 do + throw (IO.userError "status table changed") +''') + result = lean(two_roots) + if result.returncode: + raise SystemExit(result.stdout + result.stderr) + print("pass two-roots") + for name, extra, needle in [ + ("duplicate-root", " function f(Mkt, bytes32, address)\n function f(Mkt, bytes32, address)\n", "imported twice"), + ("unknown-root", " function nope(bytes32)\n", "no function C.nope(bytes32)")]: + bad = WORK / f"base/{name}.lean" + bad.write_text(HEADER.format(root=WORK / "base").replace(" function f(Mkt, bytes32, address)\n", extra)) + blob = lean(bad) + if blob.returncode == 0 or needle not in blob.stdout + blob.stderr: + raise SystemExit(f"{name}: expected rejection containing {needle!r}\n{blob.stdout}{blob.stderr}") + print(f"pass {name}") + # Generated typed accessors must not shadow `model`, `report`, ... + clash = write_project("accessor-collision", lambda d: (d / "Slice.sol").write_text( + (d / "Slice.sol").read_text().replace("function lossOf(", "function model("))) + bad = clash / "Clash.lean" + bad.write_text(HEADER.format(root=clash).replace( + " function f(Mkt, bytes32, address)\n", " function model(bytes32)\n")) + blob = lean(bad) + if blob.returncode == 0 or "collides with the generated" not in blob.stdout + blob.stderr: + raise SystemExit(f"accessor-collision: expected rejection\n{blob.stdout}{blob.stderr}") + print("pass accessor-collision") + # An unsupported construct reached from the second root names that root. + second = write_project("second-root", call_unused_from_loss) + bad = second / "SecondRoot.lean" + bad.write_text(HEADER.format(root=second).replace( + " function f(Mkt, bytes32, address)\n", + " function f(Mkt, bytes32, address)\n function lossOf(bytes32)\n")) + blob = lean(bad) + text = blob.stdout + blob.stderr + if blob.returncode == 0 or "closure: C.lossOf -> L.unused" not in text: + raise SystemExit(f"second-root-unsupported: expected the lossOf call path\n{text}") + print("pass second-root-unsupported") + repeated = expect_success("deterministic", WORK / "base", witness=True) + if digest_of(base) != digest_of(repeated): + raise SystemExit("identical input did not reproduce the digest") + rounding = expect_failure("rounding-detected", write_project("rounding", change_rounding), "witness changed") + expect_failure("factor-detected", write_project("factor", change_factor), "witness changed") + expect_failure("field-detected", write_project("field", change_field), "witness changed") + expect_failure("return-order", write_project("returns", swap_returns), "witness changed") + expect_failure("layout", write_project("layout", swap_layout), "witness changed") + diagnostic = expect_failure("reachable-for", write_project("reachable", call_unused), "unsupported") + assert "[solidity-import:unsupported]" in diagnostic and "closure:" in diagnostic + assert "Lib.sol:" in diagnostic and "closure: C.f -> L.unused" in diagnostic, diagnostic + if digest_of(base) == digest_of(rounding): + raise SystemExit("helper change did not move the digest") + print("pass helper-digest") + print("import mutations passed") + + +if __name__ == "__main__": + main() diff --git a/scripts/test_check_axiomatized_primitive_boundary_sync.py b/scripts/test_check_axiomatized_primitive_boundary_sync.py deleted file mode 100644 index cdb96b46e6..0000000000 --- a/scripts/test_check_axiomatized_primitive_boundary_sync.py +++ /dev/null @@ -1,84 +0,0 @@ -from __future__ import annotations - -import io -import json -import sys -import tempfile -import textwrap -import unittest -from contextlib import redirect_stderr, redirect_stdout -from pathlib import Path - -SCRIPT_DIR = Path(__file__).resolve().parent -if str(SCRIPT_DIR) not in sys.path: - sys.path.insert(0, str(SCRIPT_DIR)) - -import docsync - - -class AxiomatizedPrimitiveBoundarySyncTests(unittest.TestCase): - def _write_fixture_tree( - self, - root: Path, - *, - proof_status: str, - basic_status: str, - fuel_status: str, - edsl_doc: str, - compiler_doc: str, - solidity_guide: str, - ) -> None: - matrix = { - "expr_features": [ - { - "feature": "keccak256", - "proof_status": proof_status, - "SourceInterpreter_basic": basic_status, - "SourceInterpreter_fuel": fuel_status, - } - ], - "stmt_features": [], - } - feature_matrix = root / "artifacts" / "interpreter_feature_matrix.json" - feature_matrix.parent.mkdir(parents=True, exist_ok=True) - feature_matrix.write_text(json.dumps(matrix), encoding="utf-8") - - edsl_path = root / "docs-site" / "content" / "edsl" / "external-calls.mdx" - edsl_path.parent.mkdir(parents=True, exist_ok=True) - edsl_path.write_text(edsl_doc, encoding="utf-8") - - compiler_path = root / "docs-site" / "content" / "compiler.mdx" - compiler_path.parent.mkdir(parents=True, exist_ok=True) - compiler_path.write_text(compiler_doc, encoding="utf-8") - - guide_path = root / "docs-site" / "content" / "guides" / "solidity-to-verity.mdx" - guide_path.parent.mkdir(parents=True, exist_ok=True) - guide_path.write_text(solidity_guide, encoding="utf-8") - - def _run_check( - self, - *, - proof_status: str, - basic_status: str, - fuel_status: str, - edsl_doc: str, - compiler_doc: str, - solidity_guide: str, - ) -> tuple[int, str]: - with tempfile.TemporaryDirectory() as tmpdir: - root = Path(tmpdir) - self._write_fixture_tree( - root, - proof_status=proof_status, - basic_status=basic_status, - fuel_status=fuel_status, - edsl_doc=edsl_doc, - compiler_doc=compiler_doc, - solidity_guide=solidity_guide, - ) - - stdout = io.StringIO() - stderr = io.StringIO() - with redirect_stdout(stdout), redirect_stderr(stderr): - rc = docsync.run_entry("axiomatized_primitive_boundary", root=root) - return rc, stdout.getvalue() + stderr.getvalue() diff --git a/scripts/test_check_axioms.py b/scripts/test_check_axioms.py index 6401f7532e..77e1cd276e 100644 --- a/scripts/test_check_axioms.py +++ b/scripts/test_check_axioms.py @@ -80,7 +80,8 @@ def test_main_reports_missing_registry_entries_and_count_drift(self) -> None: "axiom documented_axiom : True\naxiom missing_axiom : True\n", encoding="utf-8", ) - (root / "AXIOMS.md").write_text( + (root / "docs").mkdir(parents=True, exist_ok=True) + (root / "docs" / "AXIOMS.md").write_text( "\n".join( [ "### 1. `documented_axiom`", diff --git a/scripts/test_check_interpreter_feature_boundary_catalog_sync.py b/scripts/test_check_interpreter_feature_boundary_catalog_sync.py deleted file mode 100644 index d302d6129b..0000000000 --- a/scripts/test_check_interpreter_feature_boundary_catalog_sync.py +++ /dev/null @@ -1,38 +0,0 @@ -#!/usr/bin/env python3 -from __future__ import annotations - -import io -import json -import sys -import tempfile -import unittest -from contextlib import redirect_stderr, redirect_stdout -from pathlib import Path - -SCRIPT_DIR = Path(__file__).resolve().parent -if str(SCRIPT_DIR) not in sys.path: - sys.path.insert(0, str(SCRIPT_DIR)) - -import docsync - - -class InterpreterFeatureBoundaryCatalogSyncTests(unittest.TestCase): - def _write_fixture_tree(self, root: Path, *, matrix: dict, doc_text: str) -> None: - feature_matrix = root / "artifacts" / "interpreter_feature_matrix.json" - feature_matrix.parent.mkdir(parents=True, exist_ok=True) - feature_matrix.write_text(json.dumps(matrix), encoding="utf-8") - - target_doc = root / "docs" / "INTERPRETER_FEATURE_MATRIX.md" - target_doc.parent.mkdir(parents=True, exist_ok=True) - target_doc.write_text(doc_text, encoding="utf-8") - - def _run_check(self, *, matrix: dict, doc_text: str) -> tuple[int, str]: - with tempfile.TemporaryDirectory() as tmpdir: - root = Path(tmpdir) - self._write_fixture_tree(root, matrix=matrix, doc_text=doc_text) - - stdout = io.StringIO() - stderr = io.StringIO() - with redirect_stdout(stdout), redirect_stderr(stderr): - rc = docsync.run_entry("interpreter_feature_boundary_catalog", root=root) - return rc, stdout.getvalue() + stderr.getvalue() diff --git a/scripts/test_check_layer2_boundary_sync.py b/scripts/test_check_layer2_boundary_sync.py deleted file mode 100644 index 85ce293f7d..0000000000 --- a/scripts/test_check_layer2_boundary_sync.py +++ /dev/null @@ -1,101 +0,0 @@ -from __future__ import annotations - -import io -import sys -import tempfile -import unittest -from contextlib import redirect_stderr, redirect_stdout -from pathlib import Path - -SCRIPT_DIR = Path(__file__).resolve().parent -if str(SCRIPT_DIR) not in sys.path: - sys.path.insert(0, str(SCRIPT_DIR)) - -import docsync - -ENTRY = docsync.get_entry("layer2_boundary") - - -class Layer2BoundarySyncTests(unittest.TestCase): - def _write_fixture_tree(self, root: Path, *, use_expected: bool, add_forbidden: bool) -> None: - expected = ENTRY.required - forbidden = ENTRY.forbidden - for label, rel_path in ENTRY.targets.items(): - target = root / rel_path - target.parent.mkdir(parents=True, exist_ok=True) - parts: list[str] = [] - if use_expected: - parts.extend(expected.get(label, [])) - if add_forbidden: - parts.extend(forbidden.get(label, [])) - if not parts: - parts.append(f"{label} placeholder") - target.write_text("\n".join(parts) + "\n", encoding="utf-8") - - def _run_check(self, *, use_expected: bool, add_forbidden: bool) -> tuple[int, str]: - with tempfile.TemporaryDirectory() as tmpdir: - root = Path(tmpdir) - self._write_fixture_tree(root, use_expected=use_expected, add_forbidden=add_forbidden) - - stdout = io.StringIO() - stderr = io.StringIO() - with redirect_stdout(stdout), redirect_stderr(stderr): - rc = docsync.run_entry("layer2_boundary", root=root) - return rc, stdout.getvalue() + stderr.getvalue() - - def test_matching_docs_pass(self) -> None: - rc, output = self._run_check(use_expected=True, add_forbidden=False) - self.assertEqual(rc, 0, output) - self.assertIn("layer2 boundary sync passed", output) - - def test_missing_required_snippet_fails(self) -> None: - rc, output = self._run_check(use_expected=False, add_forbidden=False) - self.assertEqual(rc, 1) - self.assertIn("missing `### 1. `solidityMappingSlot_lt_evmModulus` (eliminated)`", output) - - def test_forbidden_overclaim_fails(self) -> None: - rc, output = self._run_check(use_expected=True, add_forbidden=True) - self.assertEqual(rc, 1) - self.assertIn("still over-claims the Layer 2 boundary", output) - - def test_detects_stale_axiom_count_language(self) -> None: - with tempfile.TemporaryDirectory() as tmpdir: - root = Path(tmpdir) - self._write_fixture_tree(root, use_expected=True, add_forbidden=False) - target = root / ENTRY.targets["ROOT_README"] - target.write_text( - target.read_text(encoding="utf-8").replace( - "0 axioms", - "2 axioms", - ), - encoding="utf-8", - ) - - stdout = io.StringIO() - stderr = io.StringIO() - with redirect_stdout(stdout), redirect_stderr(stderr): - rc = docsync.run_entry("layer2_boundary", root=root) - output = stdout.getvalue() + stderr.getvalue() - - self.assertEqual(rc, 1) - self.assertIn("missing `0 axioms", output) - - def test_compiler_proofs_readme_stale_axiom_wording_is_forbidden(self) -> None: - forbidden = ENTRY.forbidden - self.assertIn("COMPILER_PROOFS_README", forbidden) - self.assertIn( - "it still depends on 2 documented axioms in `Compiler.Proofs.IRGeneration.Function`", - forbidden["COMPILER_PROOFS_README"], - ) - - def test_repository_docs_are_currently_in_sync(self) -> None: - stdout = io.StringIO() - stderr = io.StringIO() - with redirect_stdout(stdout), redirect_stderr(stderr): - rc = docsync.run_entry("layer2_boundary") - output = stdout.getvalue() + stderr.getvalue() - self.assertEqual(rc, 0, output) - - -if __name__ == "__main__": - unittest.main() diff --git a/scripts/test_check_linear_memory_boundary_sync.py b/scripts/test_check_linear_memory_boundary_sync.py deleted file mode 100644 index 6b7976b4ee..0000000000 --- a/scripts/test_check_linear_memory_boundary_sync.py +++ /dev/null @@ -1,102 +0,0 @@ -from __future__ import annotations - -import io -import json -import sys -import tempfile -import textwrap -import unittest -from contextlib import redirect_stderr, redirect_stdout -from pathlib import Path - -SCRIPT_DIR = Path(__file__).resolve().parent -if str(SCRIPT_DIR) not in sys.path: - sys.path.insert(0, str(SCRIPT_DIR)) - -import docsync - - -class LinearMemoryBoundarySyncTests(unittest.TestCase): - def _write_fixture_tree( - self, - root: Path, - *, - expr_proof_status: str, - expr_basic_status: str, - expr_fuel_status: str, - stmt_proof_status: str, - stmt_basic_status: str, - stmt_fuel_status: str, - edsl_doc: str, - compiler_doc: str, - solidity_guide: str, - ) -> None: - matrix = { - "expr_features": [ - { - "feature": feature, - "proof_status": expr_proof_status, - "SourceInterpreter_basic": expr_basic_status, - "SourceInterpreter_fuel": expr_fuel_status, - } - for feature in ("mload", "returndataOptionalBoolAt") - ], - "stmt_features": [ - { - "feature": feature, - "proof_status": stmt_proof_status, - "SourceInterpreter_basic": stmt_basic_status, - "SourceInterpreter_fuel": stmt_fuel_status, - } - for feature in ("mstore", "calldatacopy", "returndataCopy") - ], - } - feature_matrix = root / "artifacts" / "interpreter_feature_matrix.json" - feature_matrix.parent.mkdir(parents=True, exist_ok=True) - feature_matrix.write_text(json.dumps(matrix), encoding="utf-8") - - edsl_path = root / "docs-site" / "content" / "edsl" / "external-calls.mdx" - edsl_path.parent.mkdir(parents=True, exist_ok=True) - edsl_path.write_text(edsl_doc, encoding="utf-8") - - compiler_path = root / "docs-site" / "content" / "compiler.mdx" - compiler_path.parent.mkdir(parents=True, exist_ok=True) - compiler_path.write_text(compiler_doc, encoding="utf-8") - - guide_path = root / "docs-site" / "content" / "guides" / "solidity-to-verity.mdx" - guide_path.parent.mkdir(parents=True, exist_ok=True) - guide_path.write_text(solidity_guide, encoding="utf-8") - - def _run_check( - self, - *, - expr_proof_status: str, - expr_basic_status: str, - expr_fuel_status: str, - stmt_proof_status: str, - stmt_basic_status: str, - stmt_fuel_status: str, - edsl_doc: str, - compiler_doc: str, - solidity_guide: str, - ) -> tuple[int, str]: - with tempfile.TemporaryDirectory() as tmpdir: - root = Path(tmpdir) - self._write_fixture_tree( - root, - expr_proof_status=expr_proof_status, - expr_basic_status=expr_basic_status, - expr_fuel_status=expr_fuel_status, - stmt_proof_status=stmt_proof_status, - stmt_basic_status=stmt_basic_status, - stmt_fuel_status=stmt_fuel_status, - edsl_doc=edsl_doc, - compiler_doc=compiler_doc, - solidity_guide=solidity_guide, - ) - - stdout = io.StringIO() - stderr = io.StringIO() - with redirect_stdout(stdout), redirect_stderr(stderr): - rc = docsync.run_entry("linear_memory_boundary", root=root) - return rc, stdout.getvalue() + stderr.getvalue() diff --git a/scripts/test_check_low_level_call_boundary_sync.py b/scripts/test_check_low_level_call_boundary_sync.py deleted file mode 100644 index f0d0d8f186..0000000000 --- a/scripts/test_check_low_level_call_boundary_sync.py +++ /dev/null @@ -1,84 +0,0 @@ -from __future__ import annotations - -import io -import json -import sys -import tempfile -import textwrap -import unittest -from contextlib import redirect_stderr, redirect_stdout -from pathlib import Path - -SCRIPT_DIR = Path(__file__).resolve().parent -if str(SCRIPT_DIR) not in sys.path: - sys.path.insert(0, str(SCRIPT_DIR)) - -import docsync - - -class LowLevelCallBoundarySyncTests(unittest.TestCase): - def _write_fixture_tree( - self, - root: Path, - *, - proof_status: str, - basic_status: str, - fuel_status: str, - edsl_doc: str, - compiler_doc: str, - solidity_guide: str, - ) -> None: - matrix = { - "expr_features": [ - { - "feature": feature, - "proof_status": proof_status, - "SourceInterpreter_basic": basic_status, - "SourceInterpreter_fuel": fuel_status, - } - for feature in ("call", "staticcall", "delegatecall") - ] - } - feature_matrix = root / "artifacts" / "interpreter_feature_matrix.json" - feature_matrix.parent.mkdir(parents=True, exist_ok=True) - feature_matrix.write_text(json.dumps(matrix), encoding="utf-8") - - edsl_path = root / "docs-site" / "content" / "edsl" / "external-calls.mdx" - edsl_path.parent.mkdir(parents=True, exist_ok=True) - edsl_path.write_text(edsl_doc, encoding="utf-8") - - compiler_path = root / "docs-site" / "content" / "compiler.mdx" - compiler_path.parent.mkdir(parents=True, exist_ok=True) - compiler_path.write_text(compiler_doc, encoding="utf-8") - - guide_path = root / "docs-site" / "content" / "guides" / "solidity-to-verity.mdx" - guide_path.parent.mkdir(parents=True, exist_ok=True) - guide_path.write_text(solidity_guide, encoding="utf-8") - - def _run_check( - self, - *, - proof_status: str, - basic_status: str, - fuel_status: str, - edsl_doc: str, - compiler_doc: str, - solidity_guide: str, - ) -> tuple[int, str]: - with tempfile.TemporaryDirectory() as tmpdir: - root = Path(tmpdir) - self._write_fixture_tree( - root, - proof_status=proof_status, - basic_status=basic_status, - fuel_status=fuel_status, - edsl_doc=edsl_doc, - compiler_doc=compiler_doc, - solidity_guide=solidity_guide, - ) - - stdout = io.StringIO() - stderr = io.StringIO() - with redirect_stdout(stdout), redirect_stderr(stderr): - rc = docsync.run_entry("low_level_call_boundary", root=root) - return rc, stdout.getvalue() + stderr.getvalue() diff --git a/scripts/test_check_parity_pack_metrics.py b/scripts/test_check_parity_pack_metrics.py deleted file mode 100644 index ee98b4c54e..0000000000 --- a/scripts/test_check_parity_pack_metrics.py +++ /dev/null @@ -1,71 +0,0 @@ -from __future__ import annotations - -import json -import subprocess -import tempfile -import unittest -from pathlib import Path - -SCRIPT = Path(__file__).resolve().parent / "check_parity_pack_metrics.py" - - -class CheckParityPackMetricsTests(unittest.TestCase): - def _write_report(self, by_kind: dict[str, int]) -> str: - payload = { - "summary": { - "byKind": by_kind, - } - } - tmp = tempfile.NamedTemporaryFile("w", suffix=".json", delete=False) - json.dump(payload, tmp) - tmp.write("\n") - tmp.close() - return tmp.name - - def _run(self, *args: str) -> subprocess.CompletedProcess[str]: - return subprocess.run( - ["python3", str(SCRIPT), *args], - text=True, - capture_output=True, - check=False, - ) - - def test_passes_when_within_bounds(self) -> None: - report = self._write_report( - { - "missing_in_solc": 0, - "missing_in_verity": 0, - "line_mismatch": 0, - } - ) - proc = self._run("--report", report) - self.assertEqual(proc.returncode, 0, proc.stderr) - self.assertIn("onlyInVerity=0", proc.stdout) - self.assertIn("onlyInSolidity=0", proc.stdout) - self.assertIn("hashMismatch=0", proc.stdout) - - def test_fails_when_metric_exceeds_threshold(self) -> None: - report = self._write_report({"line_mismatch": 2}) - proc = self._run("--report", report, "--max-hash-mismatch", "1") - self.assertNotEqual(proc.returncode, 0) - self.assertIn("hashMismatch=2 exceeds max 1", proc.stdout) - - def test_markdown_output(self) -> None: - report = self._write_report({"missing_in_solc": 1, "line_mismatch": 3}) - proc = self._run( - "--report", - report, - "--max-only-in-verity", - "1", - "--max-hash-mismatch", - "3", - "--format", - "markdown", - ) - self.assertEqual(proc.returncode, 0, proc.stdout) - self.assertIn("### Parity Pack Metrics", proc.stdout) - self.assertIn("`onlyInVerity`", proc.stdout) - - -if __name__ == "__main__": - unittest.main() diff --git a/scripts/test_check_spec_named_storage.py b/scripts/test_check_spec_named_storage.py deleted file mode 100644 index 74844b03e7..0000000000 --- a/scripts/test_check_spec_named_storage.py +++ /dev/null @@ -1,124 +0,0 @@ -#!/usr/bin/env python3 -"""Tests for the spec named-storage gate.""" - -from __future__ import annotations - -import contextlib -import io -import tempfile -import unittest -from pathlib import Path - -import check_spec_named_storage - -NAMED_SPEC = '''\ -namespace Contracts.VaultFromSolidity.Spec -/-- Never write `s.readSlot 0` or `ContractState.readMap s 2 a` here. -/ -def solvent (v : Storage) : Prop := v.totalAssets = v.totalSupply -def balanceOf_spec (account : Address) (result : Uint256) (v : Storage) : Prop := - result = v.shareBalances account -def depositFits (amount : Uint256) (v : Storage) : Prop := - v.totalAssets.val + amount.val ≤ Verity.Core.MAX_UINT256 -def label : String := "readMap 2" -def storage2 (v : Storage) := v.totalSupply -def readSlotted (v : Storage) := v.totalSupply -end Contracts.VaultFromSolidity.Spec -''' - -RAW_SPEC = '''\ -namespace Contracts.VaultFromSolidity.Spec -def solvent (s : ContractState) : Prop := s.readSlot 0 = s.readSlot 1 -def balance (s : Storage) (a : Address) := ContractState.readMap s 2 a -def raw (s : Storage) := s.storage (1) -def ghost (s : Storage) := s.knownAddresses -end Contracts.VaultFromSolidity.Spec -''' - -# Each of these addressed storage by number while passing the first version of -# the gate, which only looked for a numeric literal right after a short list of -# accessor names on the same line. -BYPASSES = ( - ("def f (v : Storage) := v.readMapUint 3 k", "raw `ContractState` accessor `readMapUint`"), - ("def f (v : Storage) := v.readTransient 1", "raw `ContractState` accessor `readTransient`"), - ("def f (v : Storage) := v.readSlot\n 0", "raw `ContractState` accessor `readSlot`"), - ("def f (s : Storage) := ContractState.readSlot (s) 2", "`ContractState` named directly"), - ("def f (s : Storage) := s.readSlot <| 0", "raw `ContractState` accessor `readSlot`"), - ("def f (s : Storage) := (s.storage) 0", "raw `ContractState` accessor `storage`"), - ("def f (s : Storage) := s.readSlot totalAssetsSlot.slot", "raw `ContractState` accessor `readSlot`"), - ("def f (s : Storage) := s.storageWords key", "raw storage field `storageWords`"), - ("def f (s : Storage) := s.1 0", "positional projection `.1`"), - ("def f (s : Storage) := Storage.casesOn s fun w _ _ => w", "structure eliminator `Storage.casesOn`"), - ("def f (s : Storage) := Verity.ContractState.readSlot s 0", "`ContractState` named directly"), -) - - -class SpecNamedStorageTests(unittest.TestCase): - def run_gate(self, spec: str) -> tuple[int, str]: - with tempfile.TemporaryDirectory() as tmpdir: - root = Path(tmpdir) - for rel in check_spec_named_storage.SPEC_FILES: - target = root / rel - target.parent.mkdir(parents=True, exist_ok=True) - target.write_text(spec, encoding="utf-8") - old_root = check_spec_named_storage.ROOT - output = io.StringIO() - try: - check_spec_named_storage.ROOT = root - with contextlib.redirect_stdout(output), contextlib.redirect_stderr(output): - status = check_spec_named_storage.main() - finally: - check_spec_named_storage.ROOT = old_root - return status, output.getvalue() - - def test_accessors_come_from_core(self) -> None: - accessors = check_spec_named_storage.RAW_ACCESSORS - for name in ("readSlot", "writeSlot", "readMap", "writeMap", "readMapUint", - "readTransient", "storage", "storageMap", "readArray"): - self.assertIn(name, accessors) - self.assertNotIn("Contract", accessors) - self.assertEqual(check_spec_named_storage.raw_accessors("def readSlot := 0\n"), frozenset()) - - def test_named_spec_passes(self) -> None: - status, output = self.run_gate(NAMED_SPEC) - self.assertEqual(status, 0, output) - - def test_raw_slot_spec_fails(self) -> None: - status, output = self.run_gate(RAW_SPEC) - self.assertEqual(status, 1) - self.assertEqual( - check_spec_named_storage.find_violations(RAW_SPEC), - [ - (2, "raw `ContractState` accessor `readSlot`"), - (2, "raw `ContractState` accessor `readSlot`"), - (2, "`ContractState` named directly"), - (3, "raw `ContractState` accessor `readMap`"), - (3, "`ContractState` named directly"), - (4, "raw `ContractState` accessor `storage`"), - (5, "`knownAddresses` ghost bookkeeping"), - ], - ) - self.assertIn("Spec.lean:2:", output) - - def test_bypasses_are_rejected(self) -> None: - for spec, message in BYPASSES: - with self.subTest(spec=spec): - messages = [m for _, m in check_spec_named_storage.find_violations(spec)] - self.assertIn(message, messages) - self.assertEqual(self.run_gate(spec)[0], 1) - - def test_missing_opted_in_file_fails(self) -> None: - old_root = check_spec_named_storage.ROOT - output = io.StringIO() - with tempfile.TemporaryDirectory() as tmpdir: - try: - check_spec_named_storage.ROOT = Path(tmpdir) - with contextlib.redirect_stderr(output): - status = check_spec_named_storage.main() - finally: - check_spec_named_storage.ROOT = old_root - self.assertEqual(status, 1) - self.assertIn("opted-in spec file is missing", output.getvalue()) - - -if __name__ == "__main__": - unittest.main() diff --git a/scripts/test_check_storage_layout.py b/scripts/test_check_storage_layout.py index fc8811b1ea..dd83d1729f 100644 --- a/scripts/test_check_storage_layout.py +++ b/scripts/test_check_storage_layout.py @@ -12,40 +12,10 @@ from check_storage_layout import ( check_spec_edsl_consistency, - extract_compiler_specs, extract_spec_slots, ) -class CheckStorageLayoutExtractCompilerSpecsTests(unittest.TestCase): - def test_extract_compiler_specs_supports_macro_alias_defs(self) -> None: - with tempfile.TemporaryDirectory() as tmpdir: - spec_file = Path(tmpdir) / "Specs.lean" - spec_file.write_text( - "def ownedSpec : CompilationModel := Contracts.Owned.spec\n", - encoding="utf-8", - ) - rows = extract_compiler_specs(spec_file) - - self.assertIn("Owned", rows) - self.assertEqual(rows["Owned"], [("owner", "Address", 0)]) - - def test_extract_compiler_specs_supports_filtered_macro_alias_defs(self) -> None: - with tempfile.TemporaryDirectory() as tmpdir: - spec_file = Path(tmpdir) / "Specs.lean" - spec_file.write_text( - "def counterSpec : CompilationModel :=\n" - " let canonical := Contracts.Counter.spec\n" - " { canonical with\n" - ' functions := canonical.functions.filter fun fn => fn.name = "increment" }\n', - encoding="utf-8", - ) - rows = extract_compiler_specs(spec_file) - - self.assertIn("Counter", rows) - self.assertEqual(rows["Counter"], [("count", "Uint256", 0)]) - - class CheckStorageLayoutSpecEdslConsistencyTests(unittest.TestCase): def test_extract_spec_slots_supports_storage_helper_forms(self) -> None: with tempfile.TemporaryDirectory() as tmpdir: @@ -73,16 +43,25 @@ def test_extract_spec_slots_supports_storage_helper_forms(self) -> None: def test_check_spec_edsl_consistency_reports_missing_spec_slots(self) -> None: errors = check_spec_edsl_consistency( edsl={"Owned": [("owner", "Address", 0)]}, - spec={"Owned": []}, - compiler={"Owned": [("owner", "Address", 0)]}, - compiler_externals={}, + spec={"Owned": [("slot1", "uint256", 1)]}, ) self.assertEqual( errors, - ["Spec-EDSL: Owned.slot0 (address) in EDSL but not in Spec"], + [ + "Spec-EDSL: Owned.slot1 (uint256) in Spec but not in EDSL", + "Spec-EDSL: Owned.slot0 (address) in EDSL but not in Spec", + ], ) + def test_check_spec_edsl_consistency_skips_smoke_contracts(self) -> None: + errors = check_spec_edsl_consistency( + edsl={"FooSmoke": [("x", "Uint256", 0)]}, + spec={"FooSmoke": [("slot1", "uint256", 1)]}, + ) + self.assertEqual(errors, []) + + if __name__ == "__main__": unittest.main() diff --git a/scripts/test_check_struct_mapping_surface_sync.py b/scripts/test_check_struct_mapping_surface_sync.py deleted file mode 100644 index dcc2c0ad24..0000000000 --- a/scripts/test_check_struct_mapping_surface_sync.py +++ /dev/null @@ -1,158 +0,0 @@ -from __future__ import annotations - -import io -import sys -import tempfile -import textwrap -import unittest -from contextlib import redirect_stderr, redirect_stdout -from pathlib import Path - -SCRIPT_DIR = Path(__file__).resolve().parent -if str(SCRIPT_DIR) not in sys.path: - sys.path.insert(0, str(SCRIPT_DIR)) - -import docsync - - -class StructMappingSurfaceSyncTests(unittest.TestCase): - def _write_fixture_tree( - self, - root: Path, - *, - types_text: str, - roadmap: str, - compiler_doc: str, - add_contract: str, - ) -> None: - types_path = root / "Compiler" / "CompilationModel" / "Types.lean" - types_path.parent.mkdir(parents=True, exist_ok=True) - types_path.write_text(types_text, encoding="utf-8") - - roadmap_path = root / "docs" / "ROADMAP.md" - roadmap_path.parent.mkdir(parents=True, exist_ok=True) - roadmap_path.write_text(roadmap, encoding="utf-8") - - compiler_path = root / "docs-site" / "content" / "compiler.mdx" - compiler_path.parent.mkdir(parents=True, exist_ok=True) - compiler_path.write_text(compiler_doc, encoding="utf-8") - - add_contract_path = root / "docs-site" / "content" / "guides" / "add-contract.mdx" - add_contract_path.parent.mkdir(parents=True, exist_ok=True) - add_contract_path.write_text(add_contract, encoding="utf-8") - - def _run_check( - self, - *, - types_text: str, - roadmap: str, - compiler_doc: str, - add_contract: str, - ) -> tuple[int, str]: - with tempfile.TemporaryDirectory() as tmpdir: - root = Path(tmpdir) - self._write_fixture_tree( - root, - types_text=types_text, - roadmap=roadmap, - compiler_doc=compiler_doc, - add_contract=add_contract, - ) - - stdout = io.StringIO() - stderr = io.StringIO() - with redirect_stdout(stdout), redirect_stderr(stderr): - rc = docsync.run_entry("struct_mapping_surface", root=root) - return rc, stdout.getvalue() + stderr.getvalue() - - def test_docs_note_required_when_struct_mapping_surface_exists(self) -> None: - types_text = textwrap.dedent( - """ - | mappingStruct (keyType : MappingKeyType) (members : List StructMember) - | mappingStruct2 (outerKey : MappingKeyType) (innerKey : MappingKeyType) (members : List StructMember) - | structMember (field : String) (key : Expr) (memberName : String) - | structMember2 (field : String) (key1 key2 : Expr) (memberName : String) - | setStructMember (field : String) (key : Expr) (memberName : String) (value : Expr) - | setStructMember2 (field : String) (key1 key2 : Expr) (memberName : String) (value : Expr) - """ - ) - note = textwrap.dedent( - """ - FieldType.mappingStruct` / `FieldType.mappingStruct2` plus `Expr.structMember` / `Stmt.setStructMember` now make struct-valued mappings and packed submembers first-class. - `structMember "f" key "member"` - `structMember2 "f" k1 k2 "member"` - `setStructMember "f" key "member" val` - `setStructMember2 "f" k1 k2 "member" val` - For Morpho-style `mapping(K => Struct)` / `mapping(K1 => mapping(K2 => Struct))` layouts, declare `FieldType.mappingStruct` / `FieldType.mappingStruct2`. - `generate_contract.py` currently scaffolds scalar fields plus simple `mapping(address => uint256)` / `mapping(uint256 => uint256)` storage only. - For `mappingStruct` / `mappingStruct2` layouts with packed members, use the native `verity_contract` storage forms `MappingStruct(...)` / `MappingStruct2(...)` and the corresponding `structMember` / `setStructMember` operations directly. - """ - ) - rc, output = self._run_check( - types_text=types_text, - roadmap=note, - compiler_doc=note, - add_contract=note, - ) - self.assertEqual(rc, 0, output) - self.assertIn("docs note required", output) - - def test_missing_docs_note_fails_when_struct_mapping_surface_exists(self) -> None: - types_text = textwrap.dedent( - """ - | mappingStruct (keyType : MappingKeyType) (members : List StructMember) - | mappingStruct2 (outerKey : MappingKeyType) (innerKey : MappingKeyType) (members : List StructMember) - | structMember (field : String) (key : Expr) (memberName : String) - | structMember2 (field : String) (key1 key2 : Expr) (memberName : String) - | setStructMember (field : String) (key : Expr) (memberName : String) (value : Expr) - | setStructMember2 (field : String) (key1 key2 : Expr) (memberName : String) (value : Expr) - """ - ) - rc, output = self._run_check( - types_text=types_text, - roadmap="storage layout controls exist\n", - compiler_doc="mapping words exist\n", - add_contract="use the scaffold generator\n", - ) - self.assertEqual(rc, 1) - self.assertIn("compiler.mdx is out of sync", output) - - def test_partial_struct_mapping_surface_fails_closed(self) -> None: - types_text = textwrap.dedent( - """ - | mappingStruct (keyType : MappingKeyType) (members : List StructMember) - | structMember (field : String) (key : Expr) (memberName : String) - | setStructMember (field : String) (key : Expr) (memberName : String) (value : Expr) - """ - ) - rc, output = self._run_check( - types_text=types_text, - roadmap="storage layout controls exist\n", - compiler_doc="mapping words exist\n", - add_contract="use the scaffold generator\n", - ) - self.assertEqual(rc, 1) - self.assertIn("partial struct-mapping surface support", output) - self.assertIn("missing: | mappingStruct2, | structMember2, | setStructMember2", output) - - def test_docs_note_not_required_without_struct_mapping_surface(self) -> None: - rc, output = self._run_check( - types_text="| mappingTyped (mt : MappingType)\n", - roadmap="storage layout controls exist\n", - compiler_doc="mapping words exist\n", - add_contract="use the scaffold generator\n", - ) - self.assertEqual(rc, 0, output) - self.assertIn("docs note not required", output) - - def test_repository_docs_are_currently_in_sync(self) -> None: - stdout = io.StringIO() - stderr = io.StringIO() - with redirect_stdout(stdout), redirect_stderr(stderr): - rc = docsync.run_entry("struct_mapping_surface") - output = stdout.getvalue() + stderr.getvalue() - self.assertEqual(rc, 0, output) - - -if __name__ == "__main__": - unittest.main() diff --git a/scripts/test_evmyullean_fork_conformance_workflow.py b/scripts/test_evmyullean_fork_conformance_workflow.py index b0d719d119..35f6131e79 100644 --- a/scripts/test_evmyullean_fork_conformance_workflow.py +++ b/scripts/test_evmyullean_fork_conformance_workflow.py @@ -6,8 +6,8 @@ ROOT = Path(__file__).resolve().parents[1] WORKFLOW = ROOT / ".github" / "workflows" / "evmyullean-fork-conformance.yml" -TRUST_ASSUMPTIONS = ROOT / "TRUST_ASSUMPTIONS.md" -AXIOMS = ROOT / "AXIOMS.md" +TRUST_ASSUMPTIONS = ROOT / "docs" / "TRUST_ASSUMPTIONS.md" +AXIOMS = ROOT / "docs" / "AXIOMS.md" MAKEFILE = ROOT / "Makefile" NATIVE_LOWERING_REPORT = ROOT / "artifacts" / "evmyullean_native_lowering_report.json" ROADMAP = ROOT / "docs" / "ROADMAP.md" diff --git a/scripts/test_generate_yul_identity_diff_report.py b/scripts/test_generate_yul_identity_diff_report.py deleted file mode 100644 index 62b17c71d4..0000000000 --- a/scripts/test_generate_yul_identity_diff_report.py +++ /dev/null @@ -1,115 +0,0 @@ -import json -import sys -import tempfile -import unittest -from pathlib import Path - -sys.path.insert(0, str(Path(__file__).resolve().parent)) -import generate_yul_identity_diff_report as checker - - -def _write(path: Path, content: str) -> None: - path.parent.mkdir(parents=True, exist_ok=True) - path.write_text(content, encoding="utf-8") - - -class GenerateYulIdentityDiffReportTests(unittest.TestCase): - def test_identical_inputs_report_identical(self) -> None: - with tempfile.TemporaryDirectory() as tmp: - root = Path(tmp) - verity = root / "verity" - solc = root / "solc" - yul = """object "C" { - code { - function foo() { - let x := 1 - } - } -} -""" - _write(verity / "C.yul", yul) - _write(solc / "C.yul", yul) - - report = checker.build_report(verity, solc) - self.assertEqual(report["status"], "identical") - self.assertEqual(report["summary"]["totalMismatches"], 0) - self.assertEqual(report["mismatches"], []) - - def test_mismatch_localizes_to_function_scope(self) -> None: - with tempfile.TemporaryDirectory() as tmp: - root = Path(tmp) - verity = root / "verity" - solc = root / "solc" - _write( - verity / "C.yul", - """object "C" { - code { - function foo() { - let x := 1 - } - } -} -""", - ) - _write( - solc / "C.yul", - """object "C" { - code { - function foo() { - let x := 2 - } - } -} -""", - ) - - report = checker.build_report(verity, solc) - self.assertEqual(report["status"], "non_identical") - self.assertGreater(report["summary"]["totalMismatches"], 0) - first = report["mismatches"][0] - self.assertEqual(first["file"], "C.yul") - self.assertIn("function:foo", first["path"]) - - def test_repeated_runs_are_stable(self) -> None: - with tempfile.TemporaryDirectory() as tmp: - root = Path(tmp) - verity = root / "verity" - solc = root / "solc" - out1 = root / "r1.json" - out2 = root / "r2.json" - - _write( - verity / "C.yul", - """object "C" { - code { - function foo() { - let x := 1 - } - } -} -""", - ) - _write( - solc / "C.yul", - """object "C" { - code { - function foo() { - let x := 2 - } - } -} -""", - ) - - rc1 = checker.main(["--verity-dir", str(verity), "--solc-dir", str(solc), "--output", str(out1)]) - rc2 = checker.main(["--verity-dir", str(verity), "--solc-dir", str(solc), "--output", str(out2)]) - self.assertEqual(rc1, 0) - self.assertEqual(rc2, 0) - - j1 = json.loads(out1.read_text(encoding="utf-8")) - j2 = json.loads(out2.read_text(encoding="utf-8")) - self.assertEqual(j1, j2) - - -if __name__ == "__main__": - unittest.main() diff --git a/scripts/test_lean_lint.py b/scripts/test_lean_lint.py index a39f3eaa09..4f170d47ea 100644 --- a/scripts/test_lean_lint.py +++ b/scripts/test_lean_lint.py @@ -24,7 +24,6 @@ def test_expected_rules_registered(self) -> None: "split_compiler_test_artifacts", "rewrite_proof_metadata", "proof_length", - "spec_named_storage", }, ) diff --git a/scripts/test_solidity_differential.py b/scripts/test_solidity_differential.py new file mode 100644 index 0000000000..dd4e81931b --- /dev/null +++ b/scripts/test_solidity_differential.py @@ -0,0 +1,96 @@ +"""Protocol, determinism and failure classification tests (stdlib only).""" +import json +from pathlib import Path +import tempfile +import unittest +from unittest.mock import patch + +from solidity_differential.cases import generate, canonical +from solidity_differential.engine import HarnessError, parse_evm, execute +from solidity_differential.programs import generated_campaign, source, smaller_expressions +from solidity_differential.reduce import reduce_failure, signature + + +class SolidityDifferentialTests(unittest.TestCase): + def test_seed_reproduces_every_input(self): + config = {"variables": {"a": 8, "b": 16}, "ordered_pairs": []} + first = generate(config, 100, 17, []) + self.assertEqual(first, generate(config, 100, 17, [])) + self.assertNotEqual(first, generate(config, 100, 18, [])) + self.assertTrue(all(0 <= v["a"] < 256 and 0 <= v["b"] < 65536 for _, v in first)) + self.assertEqual(len({name for name, _ in first}), 100) + + def test_corpus_and_defaults_are_preserved(self): + result = generate({"variables": {"id": 256, "x": 8}, "defaults": {"id": 7}}, 0, 0, [{"name": "A", "x": "3"}]) + self.assertEqual(result, [("A", {"id": 7, "x": 3})]) + + def test_nested_abi_signature(self): + self.assertEqual(canonical({"type": "tuple[]", "components": [{"type": "uint256"}, {"type": "address"}]}), "(uint256,address)[]") + + def test_missing_bad_or_truncated_results_are_errors(self): + with tempfile.TemporaryDirectory() as directory: + path = Path(directory) / "results" + for text in ("", "0 timeout 0x\n", "1 revert 0x\n", "0 ok 0x01\n", "0 revert 0x 1\n"): + path.write_text(text) + with self.assertRaises(HarnessError): + parse_evm(path, [{"id": "A", "observe": []}]) + path.write_text("0 revert 0x\n") + self.assertEqual(parse_evm(path, [{"id": "A", "observe": []}])[0]["status"], "revert") + + def test_empty_or_stale_campaign_cannot_pass(self): + with tempfile.TemporaryDirectory() as directory: + path = Path(directory) + (path / "manifest.json").write_text(json.dumps({"implementationHashes": {}})) + with patch("solidity_differential.engine.implementation_hashes", return_value={"changed": "x"}): + with self.assertRaisesRegex(HarnessError, "implementation changed"): + execute(path, []) + with patch("solidity_differential.engine.implementation_hashes", return_value={}): + with self.assertRaisesRegex(HarnessError, "empty campaign"): + execute(path, []) + + def test_modified_bytecode_is_rejected_before_execution(self): + with tempfile.TemporaryDirectory() as directory: + path = Path(directory) + (path / "source.bin").write_bytes(b"changed") + (path / "manifest.json").write_text(json.dumps({ + "implementationHashes": {}, "artifactHashes": {"source.bin": "original"}})) + with patch("solidity_differential.engine.implementation_hashes", return_value={}): + with self.assertRaisesRegex(HarnessError, "campaign artifact changed"): + execute(path, [{"id": "A"}]) + + def test_program_reduction_preserves_typed_tree(self): + tree = {"kind": "binary", "op": "*", "left": {"kind": "x"}, "right": {"kind": "y"}} + self.assertIn({"kind": "x"}, list(smaller_expressions(tree))) + rendered = source({"bits": 248, "expression": tree, "helper": True, "renamed": False, "projection_collision": True}) + self.assertIn("uint248(x)", rendered) + self.assertIn("_verity_slice.tmp_0", rendered) + self.assertIn("L.work", rendered) + + def test_reducer_distinguishes_status_and_return_mismatches(self): + a = {"status": "ok", "words": ["1"], "storage": [], "data": "0x01"} + wrong_return = {**a, "words": ["2"]} + reverted = {**a, "status": "revert", "words": []} + self.assertNotEqual(signature({"source": a, "model": wrong_return, "compiled": a}), + signature({"source": a, "model": reverted, "compiled": a})) + + def test_metamorphic_divergence_is_recorded_and_not_reduced(self): + def fake_campaign(fixture, run, cases, seed): + out = Path(run) / "out" + out.mkdir(parents=True, exist_ok=True) + rows = "0 ok 0x01\n" if "program-0-0" in str(run) else "0 revert 0x\n" + (out / "source.txt").write_text(rows) + return {"cases": 1, "divergences": []} + + with tempfile.TemporaryDirectory() as directory: + with patch("solidity_differential.programs.campaign", fake_campaign): + report = generated_campaign(directory, 1, 0, 0) + evidence = json.loads((Path(directory) / "metamorphic-divergence.json").read_text()) + self.assertEqual(report["divergences"], [evidence]) + self.assertEqual(evidence["rows"], [{"line": 0, "reference": "0 ok 0x01", "variant": "0 revert 0x"}]) + (Path(directory) / "results.json").write_text(json.dumps(report)) + with self.assertRaisesRegex(ValueError, "metamorphic"): + reduce_failure(directory) + + +if __name__ == "__main__": + unittest.main() diff --git a/scripts/update_doc_numbers.py b/scripts/update_doc_numbers.py index 3b7986bbc9..985a0aa485 100644 --- a/scripts/update_doc_numbers.py +++ b/scripts/update_doc_numbers.py @@ -43,7 +43,7 @@ def update_llms_txt(data: dict) -> bool: f"- **Verified Contracts**: {contract_count} ({', '.join(theorems['per_contract'].keys())})\n" f"- **Theorems**: {theorems['total']} across {theorems['categories']} categories, " f"{theorems['proven']} fully proven\n" - f"- **Axioms**: {proofs['axioms']} documented Lean axioms (see AXIOMS.md)\n" + f"- **Axioms**: {proofs['axioms']} documented Lean axioms (see docs/AXIOMS.md)\n" f"- **Tests**: {tests['foundry_functions']} Foundry tests, " f"{tests['property_functions']} property tests\n" f"- **Build**: `lake build` verifies all proofs\n" diff --git a/scripts/verify_sync_spec.json b/scripts/verify_sync_spec.json index a7365ce46f..ac7c487ec3 100644 --- a/scripts/verify_sync_spec.json +++ b/scripts/verify_sync_spec.json @@ -6,9 +6,7 @@ "docs/**", "docs-site/**", "Makefile", - "AXIOMS.md", - "README.md", - "TRUST_ASSUMPTIONS.md" + "README.md" ], "build_paths": [ ".github/actions/**", @@ -23,6 +21,11 @@ "packages/**", "lakefile.lean", "lake-manifest.json", + "scripts/solidity_differential/**", + "scripts/solidity_import_differential.py", + "scripts/check_solidity_differential.sh", + "scripts/solidity_import_mutations.py", + "scripts/setup_solc_import.py", "lean-toolchain" ], "compiler_paths": [ @@ -48,6 +51,11 @@ "scripts/property_utils.py", "lakefile.lean", "lake-manifest.json", + "scripts/solidity_differential/**", + "scripts/solidity_import_differential.py", + "scripts/check_solidity_differential.sh", + "scripts/solidity_import_mutations.py", + "scripts/setup_solc_import.py", "lean-toolchain", "foundry.toml" ], @@ -818,16 +826,18 @@ "python3 scripts/generate_evmyullean_native_lowering_report.py --check", "python3 scripts/generate_print_axioms.py --check", "python3 scripts/lean_lint.py --only proof_length", - "python3 scripts/lean_lint.py --only spec_named_storage", "python3 scripts/check_issue_1060_integrity.py", "python3 -m unittest discover -s scripts -p 'test_*.py' -v" ], "expected_checks_other_commands": [], "expected_build_commands": [ - "check_lean_warning_regression.py --log lake-build.log" + "check_lean_warning_regression.py --log lake-build.log", + "setup_solc_import.py", + "solidity_import_mutations.py" ], "required_build_run_commands": [ - "lake build PrintAxioms" + "lake build PrintAxioms", + "lake build SolidityImportSmoke" ], "expected_build_audit_commands": [ "check_split_package_builds.py", @@ -907,6 +917,9 @@ "static-gas-report", "static-gas-report-patched" ], + "compiler-regressions": [ + "solidity-import-differential" + ], "lean-profile": [ "lean-perf-queue" ] @@ -930,6 +943,9 @@ "gas-report-static.tsv", "gas-report-static-patched.tsv" ], + "compiler-regressions": [ + ".lake/import-differential/**\n!.lake/import-differential/**/.lake/build/**\n!.lake/import-differential/**/.lake/packages/**" + ], "lean-profile": [ "lean-perf-queue.md" ] diff --git a/scripts/verify_sync_spec_source.py b/scripts/verify_sync_spec_source.py index 4baa0d6244..d718e07103 100644 --- a/scripts/verify_sync_spec_source.py +++ b/scripts/verify_sync_spec_source.py @@ -15,9 +15,7 @@ 'docs/**', 'docs-site/**', 'Makefile', - 'AXIOMS.md', - 'README.md', - 'TRUST_ASSUMPTIONS.md'], + 'README.md'], 'build_paths': ['.github/actions/**', '.github/workflows/verify.yml', 'Verity/**', @@ -702,12 +700,13 @@ '--check', 'python3 scripts/generate_print_axioms.py --check', 'python3 scripts/lean_lint.py --only proof_length', - 'python3 scripts/lean_lint.py --only spec_named_storage', 'python3 scripts/check_issue_1060_integrity.py', "python3 -m unittest discover -s scripts -p 'test_*.py' -v"], 'expected_checks_other_commands': [], - 'expected_build_commands': ['check_lean_warning_regression.py --log lake-build.log'], - 'required_build_run_commands': ['lake build PrintAxioms'], + 'expected_build_commands': ['check_lean_warning_regression.py --log lake-build.log', + 'setup_solc_import.py', + 'solidity_import_mutations.py'], + 'required_build_run_commands': ['lake build PrintAxioms', 'lake build SolidityImportSmoke'], 'expected_build_audit_commands': ['check_split_package_builds.py', 'check_axioms.py', 'check_proof_length.py --format=markdown >> ' @@ -930,3 +929,12 @@ def build_spec() -> dict: return copy.deepcopy(SPEC) + +# Solidity slice differential changes require the compiled-model validation lane. +for _lane in ("build_paths", "compiler_paths"): + SPEC[_lane][SPEC[_lane].index("lean-toolchain"):SPEC[_lane].index("lean-toolchain")] = ['scripts/solidity_differential/**', 'scripts/solidity_import_differential.py', 'scripts/check_solidity_differential.sh', 'scripts/solidity_import_mutations.py', 'scripts/setup_solc_import.py'] +SPEC["expected_uploaded_artifacts"]["compiler-regressions"] = ["solidity-import-differential"] +SPEC["expected_uploaded_artifact_paths"]["compiler-regressions"] = [".lake/import-differential/**\n!.lake/import-differential/**/.lake/build/**\n!.lake/import-differential/**/.lake/packages/**"] +for _key in ("expected_uploaded_artifacts", "expected_uploaded_artifact_paths"): + _profile = SPEC[_key].pop("lean-profile") + SPEC[_key]["lean-profile"] = _profile diff --git a/test/Vault.t.sol b/test/Vault.t.sol deleted file mode 100644 index c221dd2761..0000000000 --- a/test/Vault.t.sol +++ /dev/null @@ -1,99 +0,0 @@ -// SPDX-License-Identifier: MIT -pragma solidity ^0.8.33; - -import "forge-std/Test.sol"; -import "../Contracts/VaultFromSolidity/Vault.sol"; - -contract VaultTest is Test { - Vault internal vault; - address internal alice = address(0xA11CE); - address internal bob = address(0xB0B); - - function _shareBalanceSlot(address account) internal pure returns (bytes32) { - return keccak256(abi.encode(account, uint256(2))); - } - - function setUp() public { - vault = new Vault(); - } - - function test_InitialState() public view { - assertEq(vault.totalAssets(), 0); - assertEq(vault.totalSupply(), 0); - assertEq(vault.balanceOf(alice), 0); - } - - function test_DepositMintsSharesOneToOne() public { - vm.prank(alice); - vault.deposit(100); - - assertEq(vault.balanceOf(alice), 100); - assertEq(vault.totalAssets(), 100); - assertEq(vault.totalSupply(), 100); - } - - function test_WithdrawBurnsSharesOneToOne() public { - vm.startPrank(alice); - vault.deposit(100); - vault.withdraw(40); - vm.stopPrank(); - - assertEq(vault.balanceOf(alice), 60); - assertEq(vault.totalAssets(), 60); - assertEq(vault.totalSupply(), 60); - } - - function test_WithdrawRevertsWhenSharesInsufficient() public { - vm.prank(alice); - vault.deposit(10); - - vm.prank(alice); - vm.expectRevert(Vault.InsufficientShares.selector); - vault.withdraw(11); - } - - function test_WithdrawRevertsWhenAssetsInsufficient() public { - vm.store(address(vault), _shareBalanceSlot(alice), bytes32(uint256(10))); - vm.store(address(vault), bytes32(uint256(0)), bytes32(uint256(5))); - vm.store(address(vault), bytes32(uint256(1)), bytes32(uint256(10))); - - vm.prank(alice); - vm.expectRevert(Vault.InsufficientAssets.selector); - vault.withdraw(10); - } - - function test_WithdrawRevertsWhenSupplyInsufficient() public { - vm.store(address(vault), _shareBalanceSlot(alice), bytes32(uint256(10))); - vm.store(address(vault), bytes32(uint256(0)), bytes32(uint256(10))); - vm.store(address(vault), bytes32(uint256(1)), bytes32(uint256(5))); - - vm.prank(alice); - vm.expectRevert(Vault.InsufficientSupply.selector); - vault.withdraw(10); - } - - function test_MultipleDepositorsConserveTotals() public { - vm.prank(alice); - vault.deposit(70); - - vm.prank(bob); - vault.deposit(30); - - assertEq(vault.balanceOf(alice), 70); - assertEq(vault.balanceOf(bob), 30); - assertEq(vault.totalAssets(), 100); - assertEq(vault.totalSupply(), 100); - } - - function testFuzz_DepositWithdrawRoundTrip(uint128 amount) public { - vm.prank(alice); - vault.deposit(amount); - - vm.prank(alice); - vault.withdraw(amount); - - assertEq(vault.balanceOf(alice), 0); - assertEq(vault.totalAssets(), 0); - assertEq(vault.totalSupply(), 0); - } -} diff --git a/test/property_exclusions.json b/test/property_exclusions.json index 841b1ccc7f..b02ea2d277 100644 --- a/test/property_exclusions.json +++ b/test/property_exclusions.json @@ -15,37 +15,6 @@ "vaultMinimal_runtime_lowers_native", "vaultMinimal_totalAssets_nativeResultsMatchOn_revert_of_nonzero_value" ], - "VaultFromSolidity": [ - "balance_meets_spec", - "balance_success_spec", - "deposit_meets_spec", - "deposit_success_spec", - "solvent_invariant", - "withdraw_meets_spec", - "withdraw_success_spec" - ], - "SolidityImportSmoke": [ - "bump_meets_spec", - "bump_success_spec", - "dispatch_is_child", - "early_meets_spec", - "get_meets_spec", - "go_meets_spec", - "go_success_spec", - "guarded_reverts_not_owner_before_pause", - "guarded_success_spec", - "makeRev_encodes", - "make_encodes", - "other_member_unchanged_thm", - "pause_meets_spec", - "pause_success_spec", - "paused_invariant", - "set_then_get", - "snapshot_restores_status", - "status_restored", - "super_runs_parent", - "tagged_uses_base_helper" - ], "Counter": [ "getStorage_reads_count", "previewAddTwice_correct", diff --git a/test/property_manifest.json b/test/property_manifest.json index 8aa30a5928..b43566798a 100644 --- a/test/property_manifest.json +++ b/test/property_manifest.json @@ -347,28 +347,6 @@ "transfer_then_balanceOf_recipient_correct", "transfer_then_balanceOf_sender_correct" ], - "SolidityImportSmoke": [ - "bump_meets_spec", - "bump_success_spec", - "dispatch_is_child", - "early_meets_spec", - "get_meets_spec", - "go_meets_spec", - "go_success_spec", - "guarded_reverts_not_owner_before_pause", - "guarded_success_spec", - "makeRev_encodes", - "make_encodes", - "other_member_unchanged_thm", - "pause_meets_spec", - "pause_success_spec", - "paused_invariant", - "set_then_get", - "snapshot_restores_status", - "status_restored", - "super_runs_parent", - "tagged_uses_base_helper" - ], "Vault": [ "balanceOf_meets_spec", "balanceOf_preserves_state", @@ -379,14 +357,5 @@ "vaultMinimal_functions_bridged", "vaultMinimal_runtime_lowers_native", "vaultMinimal_totalAssets_nativeResultsMatchOn_revert_of_nonzero_value" - ], - "VaultFromSolidity": [ - "balance_meets_spec", - "balance_success_spec", - "deposit_meets_spec", - "deposit_success_spec", - "solvent_invariant", - "withdraw_meets_spec", - "withdraw_success_spec" ] } diff --git a/verity.svg b/verity.svg deleted file mode 100644 index 61004688d9..0000000000 --- a/verity.svg +++ /dev/null @@ -1,20 +0,0 @@ - - - - - - - - - - - - - - - - - - - -