Skip to content

Repository files navigation

yul-semantics

A formal semantics for the Yul intermediate language, written in Lean 4.

This repository defines only the Yul semantics. It is the foundation for a separate, future project — a verified optimizing compiler from Yul to EVM bytecode — that will build on top of it. The EVM bytecode semantics lives in a different repository.

Design

See DESIGN.md for the full design and its rationale. In short:

  • Gas is not modeled. Yul→Yul optimization correctness is functional equivalence, not a gas obligation.
  • The ground truth is a big-step relational semantics (an inductive evaluation relation). A fuel-indexed executable interpreter is a derived view, proven adequate.
  • The semantics is parameterized over an abstract Dialect (value type, machine state, built-in interpretation), keeping the core dialect-agnostic.
  • The EVM dialect uses BitVec 256 for words.

Building

Requires the Lean toolchain pinned in lean-toolchain (managed by elan).

lake build

What is implemented

  • Core semantics (BigStep.lean) — the big-step relational ground truth: lexical scoping, block-level function pre-collection (forward references and mutual recursion), multiple return values, and break/continue/leave/halt outcome propagation. It is a single indexed judgment (Step) over the five syntactic classes, with the five conceptual relations recovered as abbreviations.
  • Determinism (Determinism.lean) — Step.det: the judgment is deterministic given deterministic built-ins, proven by one rule induction. Discharged for the EVM dialect as EVM.run_det.
  • Executable interpreter + adequacy (Interp.lean, Adequacy.lean) — a total fuel-indexed interpreter over an ExecDialect, with a proven adequacy theorem (soundness at any fuel; completeness at sufficiently large fuel for terminating runs). Instantiated hypothesis-free for EVM as EVM.run_adequacy.
  • EVM dialect (Dialect/EVM.lean) — the full user-facing Yul EVM built-in set over BitVec 256 (through the Fusaka fork, including clz, mcopy, blobhash, blobbasefee). Covered: arithmetic/comparison/bitwise/shifts, memory (with the msize active-memory high-water mark), storage and transient storage, calldata/code/returndata reads and copies (returndatacopy bounds failure is an exceptional halt), the execution-environment and world-state readers (via abstract environment maps), logs, the object-data ops, and the halting ops. keccak256 uses an environment-supplied oracle, abstract by default and executable when a client supplies a concrete implementation.
  • Open-world calls and creationcall/callcode/delegatecall/staticcall and create/create2 are interpreted relationally by EVM.evmWithExternal calls creates. The supplied ExternalCalls/ExternalCreates relations describe completed external executions and may summarize arbitrary nested calls, creations, and re-entrant callbacks; the semantics fixes only the caller-observable boundary (memory copy-in, world commit/rollback, return-data copy-out, the success/address word). gas() is a nondeterministic oracle in these dialects. See DESIGN.md for the exact boundary.
  • Static write protection — a frame flagged static (ExecEnv.static, as set on a STATICCALL callee) enforces EVM write protection: sstore/tstore/log0log4/selfdestruct, create/create2, and value-bearing call/callcode halt exceptionally instead of modifying state; staticcall, delegatecall, and zero-value call remain permitted.
  • selfdestruct — transfers the executing account's balance and halts, recording the scheduled destruction together with its createdThisTx bit (post-EIP-6780: only an account created in the current transaction is deletable; the self-beneficiary balance-burn distinction is modeled). Actual fork-dependent deletion is a transaction-finalization step, outside this frame semantics.
  • Frame-boundary observation (Observation.lean) — revert/invalid/invalidMemoryAccess roll the frame's committed world changes back at the observation boundary (only the outcome marker and exposed return data survive), while stop/return/selfdestruct and normal termination commit — matching real EVM. Applied by EVM.committedState and the observed whole-program run EVM.RunCommitted (functional given determinism). This is what makes dead-effect reasoning sound — a dead store before a revert is observationally invisible, something raw exact-state runs cannot see; that payoff is proven in the compiler repository, whose optimizer owns the dead-effect reasoning.
  • Relational execution contracts (Contract.lean) — RunContract hides complete final-state constructors behind source-level pre/postconditions. Consequence, source-program transport, statement/sequence contracts, sequential composition, and block lifting let proofs retain only the observations they need. The Fibonacci example exposes its general correctness theorem as fibRunContract through this API.
  • Effect classification (Dialect.lean) — each built-in is classified (deterministic / reads / writes / halts). The EVM dialect proves the classification soundly over-approximates its semantics (EVM.effects_sound, and EVM.effects_sound_withExternal for the open world).
  • Objects (Object.lean, ObjectRun.lean) — the Yul object layer (nested code/data/sub-objects): name resolution, a layout-consistency predicate relating a compiler's byte layout to an object, and a symbolic proof that the canonical constructor (datacopy/return) returns a data segment's bytes.
  • Surface tooling (Syntax.lean, PrettyPrint.lean) — the yul% / yulObject% concrete-syntax DSL and a pretty-printer.

What is not (yet) done, and why

  • Yul→EVM compiler correctness. Deliberately out of scope for this repo — it belongs to the separate compiler project, which will instantiate the abstract Dialect with the real EVM semantics and prove a conditional-on-gas forward simulation. See DESIGN.md.
  • Optimization meta-theory. The pointwise equivalences, congruence lemmas, and sample rewrites live in the compiler repository, next to the optimizer whose proof obligations they carry. This repo keeps only the semantics they are stated against (Step/Run, the dialects, and the observation boundary).
  • reads-flag soundness. EVM.effects_sound proves the deterministic/writes/halts flags sound; a machine-checked soundness for reads needs a notion of state observation (a read footprint) and is deferred. The flag is documented and currently unused by any proof.
  • Separation logic and automated framing. RunContract provides a small relational Hoare layer, but it deliberately does not prescribe a heap assertion language or automate disjoint-memory framing. Those can be layered on top when a client needs them.
  • Divergence reasoning. Not needed for the main compiler theorem (the gas-metered target cannot diverge), and deferred indefinitely.
  • Gas. Not modeled by design (see DESIGN.md §1). Within-frame out-of-gas is therefore not expressible; out-of-gas in a callee is subsumed by the open-world call relation.

Scope of the meta-theory (important)

The determinism proof, the executable interpreter, and the adequacy theorem are established for the closed-world local dialect EVM.evm only. They do not extend to the open-world dialect EVM.evmWithExternal (call/create):

  • evmWithExternal is relational and may be non-deterministic (an external call/create outcome is a response chosen by an arbitrary environment), so the determinism theorem does not apply to it.
  • It has no executable interpreter and no adequacy theorem — there is deliberately no universal executable choice for an open-world relation. In the executable dialect (EVM.evm / EVM.exec), gas() and the call/create family are intentionally left stuck (no reduction).
  • What does carry over to the open world is effect-classification soundness (EVM.effects_sound_withExternal).

So do not read "deterministic" or "adequate" as statements about programs that call gas() or make external calls/creations.

Tests

Correctness is carried by the theorems above; in addition the repository is exercised end-to-end:

  • Examples.lean — interpreter runs via native_decide (arithmetic, storage, memory and msize, the returndatacopy bounds exception, selfdestruct) and yul% DSL round-trips.
  • FibExample.lean — a full worked contract (see below).
  • ObjectRun.lean — a concrete object whose layout is checked consistent and whose constructor is run to its returned data segment.
  • Dialect/EVM.lean and Observation.lean — inline guards for effect flags, the selfdestruct cases, the open-world call/create/gas() boundary, static write protection, and the commit/rollback observation.

Worked example

FibExample.lean is an end-to-end verification: a Yul contract that reads n from calldata, computes the n-th Fibonacci number, and returns it. It is proven correct two ways:

  • concretely, by running the interpreter for several inputs (native_decide); and
  • generally (fibContract_correct): for every initial state the contract halts, writes fib(n) mod 2²⁵⁶ to memory, and returns that word. The proof is fully relational — a loop invariant (fibLoop, a = fib i, b = fib(i+1)) by induction on the remaining iterations, assembled with the prelude and postlude into the whole run.

Acknowledgements

This project is inspired by EVMYulLean, Nethermind's Lean 4 formalization of the EVM and Yul — in particular, embedding Yul concrete syntax as a Lean DSL follows its spirit. The semantics here is an independent, dialect-parametric design; see DESIGN.md.

License

Apache 2.0.

About

Semantics of the Yul language in lean

Resources

Stars

5 stars

Watchers

1 watching

Forks

Releases

Packages

Contributors

Languages