Goal
Make the Verity compiler enforce safety rules that Solidity leaves to programmer discipline. Three features share a common pattern: the macro enforces a safety rule by default, and opt-out requires either a proof, a known-safe guard, or an explicit trust surface marker.
Parent issue: #1726 (Language design improvements)
Connects to: #1724 Wave 2 (control flow, modifiers)
Feature 1: CEI Enforcement with Proof-Based Opt-Out (P0)
Problem
Reentrancy remains the most iconic smart contract vulnerability class. The DAO hack ($60M, 2016), Rari Capital ($80M, 2022), and Orion Protocol ($3M, 2023) all exploited the same pattern: external calls before state updates. Solidity's mitigation is a convention (Checks-Effects-Interactions) and an opt-in library (`ReentrancyGuard`). Neither is enforced by the language.
What Verity Already Has
- `nonReentrant` primitive (`Verity/Core.lean:562`) using storage slot lock pattern
- `ReentrancyExample` contract that formally proves the vulnerable pattern cannot maintain the supply invariant, and the safe pattern provably does
Design: Hybrid Enforcement
Default behavior: The macro statically scans each function body. If any write statement (`set*`, `emit`, storage mutation) appears after an external call (`externalCall`, `call`, `staticcall`, `delegatecall`), it emits a compile-time error.
Escalation ladder for opt-out (from least to most trust):
Path A — Provide a Lean Proof (zero trust)
```
@[cei_safe by reentrancySafetyProof]
function complexWithdraw (amount : Uint256) : Unit := do
externalCall target "callback()" []
setStorage balanceSlot newBalance -- allowed: proof certifies safety
```
Where `reentrancySafetyProof` is a theorem of type:
```lean
theorem reentrancySafetyProof :
∀ s s', complexWithdraw_spec amount s s' →
invariant_preserved s s' := by ...
```
The proof is machine-checked by Lean's kernel. This is the gold standard opt-out.
Path B — Use `nonReentrant` Guard (low trust)
```
function withdraw (amount : Uint256) : Unit := do
nonReentrant lockSlot do
externalCall target "transfer()" [amount]
setStorage balanceSlot newBalance -- CEI relaxed inside nonReentrant
```
The macro recognizes `nonReentrant` as a known-safe wrapper and suppresses the CEI error inside its body. This appears in `--trust-report` as "guarded by nonReentrant".
Path C — Annotation (explicit trust surface)
```
@[allow_post_interaction_writes "reason: callback is to trusted oracle"]
function oracleUpdate () : Unit := do
...
```
Appears in `--trust-report`. Can be denied project-wide via `--deny-post-interaction-writes`.
Implementation Plan
- Static analysis (`Macro/Translate.lean` or new `Macro/CEICheck.lean`): Walk function body AST, track whether we've seen an external call, error on writes after calls
- Path A: Extend `@[cei_safe by name]` attribute — resolve `name` as a theorem in scope, type-check against expected signature
- Path B: Pattern-match `nonReentrant lockSlot do ...` in the AST walker, mark inner body as CEI-exempt
- Path C: `@[allow_post_interaction_writes "reason"]` attribute — skip CEI check, emit trust-report entry
- Trust report integration: Add CEI entries to `--trust-report` output
Estimated effort: 2–3 weeks
Feature 2: `@[requires Role]` with Auto-Generated Theorems (P0)
Problem
Access control is the #1 cause of smart contract losses ($953.2M cumulative per Hacken 2025). Solidity relies on runtime `require(msg.sender == owner)` checks that developers can forget to add.
Design
```
verity_contract GatedToken where
roles
Owner : ownerSlot
Minter : minterSlot
Pauser : pauserSlot
storage
ownerSlot : Address := slot 0
minterSlot : Address := slot 1
pauserSlot : Address := slot 2
@[requires Minter]
function mint (to : Address, amount : Uint256) : Unit := do
...
@[requires Owner]
function setMinter (newMinter : Address) : Unit := do
setStorageAddr minterSlot newMinter
-- No role annotation = public function
function balanceOf (addr : Address) : Uint256 := do
...
```
What `@[requires Role]` Generates
1. Runtime require check (injected at function entry):
```lean
require (sender == getStorageAddr ownerSlot) "Not owner"
```
Compiles to Yul:
```yul
if iszero(eq(caller(), sload(ownerSlot))) { revert(...) }
```
2. Access control theorem (auto-generated):
```lean
theorem setMinter_requires_owner :
∀ s s' sender newMinter, setMinter_spec newMinter sender s s' →
sender = s.storage ownerSlot := by ...
```
This theorem is auto-provable because the macro inserted the require. Its value:
- Auditors: machine-checked guarantee of access control
- Composability: downstream proofs can assume this theorem
- Safety net: removing the annotation removes the theorem, breaking any dependent proof → compile-time failure
Extensions
- Multi-role: `@[requires Owner | Minter]` generates OR-check
- Role hierarchies: definable in `roles` block
- Custom role resolution: `Role : expr` where `expr` is an arbitrary expression
Implementation Plan
- Syntax (`Macro/Syntax.lean`): Add `roles` section binding role names to storage slots/expressions
- Attribute (`Macro/Translate.lean`): Parse `@[requires RoleName]`, look up role → slot binding
- Code injection: Prepend `require (sender == getStorageAddr slot) "Not {role}"` to function body
- Theorem generation: Generate `functionName_requires_roleName` theorem alongside the function spec
- Validation: Error if a role name is used that wasn't declared in `roles` block
Estimated effort: 2–3 weeks
Feature 3: Explicit `unsafe` Blocks (P1)
Problem
`delegatecall`, inline assembly, raw storage access, and external calls are all available everywhere in Solidity with no distinction from safe code. There's no way to grep for "all the dangerous operations in this contract."
What Verity Already Has
- `localObligations` on functions/constructors for trust-surface marking
- `--trust-report` for machine-readable trust surface output
- `--deny-*` flags for fail-closed verification
- `SupportedSpec` gates that categorize operations by safety surface
Design
```
function upgradeImplementation (newImpl : Address) : Unit := do
@[requires Owner]
let currentImpl ← getStorage implSlot
unsafe "delegatecall to new implementation" do
delegatecall gas() newImpl 0 0 0 0
-- Back to safe code
setStorage implSlot newImpl
```
The `unsafe` keyword:
- Syntactically mandatory for: `delegatecall`, raw `mstore` to arbitrary offsets, raw `sstore`, inline Yul, `rawLog`
- Requires a string description — appears in `--trust-report`
- Searchable/greppable in audit
- Can be denied project-wide via `--deny-unsafe`
Implementation Plan
- Syntax: Add `unsafe "reason" do ...` block to the expression/statement grammar
- Validation (`Macro/Translate.lean`): Track "safe context" flag. Error if a restricted operation appears outside `unsafe`
- Trust report: Each `unsafe` block → entry in `--trust-report` with location, reason, and contained operations
- Deny flag: `--deny-unsafe` makes any `unsafe` block a compile error (for contracts that want to prove they're fully safe)
- Map to existing infra: `unsafe` blocks generate `localObligations` entries
Estimated effort: 2–3 weeks
Shared Pattern: Escalation Ladder
All three features follow the same safety philosophy:
```
SAFE BY DEFAULT (compiler enforces)
↓ opt-out via:
LEAN PROOF (machine-checked, zero trust)
↓ if proof not feasible:
KNOWN-SAFE GUARD (nonReentrant, require, etc.)
↓ if guard not applicable:
EXPLICIT ANNOTATION (trust surface marker in --trust-report)
```
This gives developers clear escape hatches while keeping the default behavior strictly safe.
Execution Order
- CEI enforcement (P0, 2–3 weeks) — highest impact per effort
- `@[requires Role]` (P0, 2–3 weeks) — can be parallelized with CEI
- `unsafe` blocks (P1, 2–3 weeks) — builds on trust-report infrastructure
Related Issues
Goal
Make the Verity compiler enforce safety rules that Solidity leaves to programmer discipline. Three features share a common pattern: the macro enforces a safety rule by default, and opt-out requires either a proof, a known-safe guard, or an explicit trust surface marker.
Parent issue: #1726 (Language design improvements)
Connects to: #1724 Wave 2 (control flow, modifiers)
Feature 1: CEI Enforcement with Proof-Based Opt-Out (P0)
Problem
Reentrancy remains the most iconic smart contract vulnerability class. The DAO hack ($60M, 2016), Rari Capital ($80M, 2022), and Orion Protocol ($3M, 2023) all exploited the same pattern: external calls before state updates. Solidity's mitigation is a convention (Checks-Effects-Interactions) and an opt-in library (`ReentrancyGuard`). Neither is enforced by the language.
What Verity Already Has
Design: Hybrid Enforcement
Default behavior: The macro statically scans each function body. If any write statement (`set*`, `emit`, storage mutation) appears after an external call (`externalCall`, `call`, `staticcall`, `delegatecall`), it emits a compile-time error.
Escalation ladder for opt-out (from least to most trust):
Path A — Provide a Lean Proof (zero trust)
```
@[cei_safe by reentrancySafetyProof]
function complexWithdraw (amount : Uint256) : Unit := do
externalCall target "callback()" []
setStorage balanceSlot newBalance -- allowed: proof certifies safety
```
Where `reentrancySafetyProof` is a theorem of type:
```lean
theorem reentrancySafetyProof :
∀ s s', complexWithdraw_spec amount s s' →
invariant_preserved s s' := by ...
```
The proof is machine-checked by Lean's kernel. This is the gold standard opt-out.
Path B — Use `nonReentrant` Guard (low trust)
```
function withdraw (amount : Uint256) : Unit := do
nonReentrant lockSlot do
externalCall target "transfer()" [amount]
setStorage balanceSlot newBalance -- CEI relaxed inside nonReentrant
```
The macro recognizes `nonReentrant` as a known-safe wrapper and suppresses the CEI error inside its body. This appears in `--trust-report` as "guarded by nonReentrant".
Path C — Annotation (explicit trust surface)
```
@[allow_post_interaction_writes "reason: callback is to trusted oracle"]
function oracleUpdate () : Unit := do
...
```
Appears in `--trust-report`. Can be denied project-wide via `--deny-post-interaction-writes`.
Implementation Plan
Estimated effort: 2–3 weeks
Feature 2: `@[requires Role]` with Auto-Generated Theorems (P0)
Problem
Access control is the #1 cause of smart contract losses ($953.2M cumulative per Hacken 2025). Solidity relies on runtime `require(msg.sender == owner)` checks that developers can forget to add.
Design
```
verity_contract GatedToken where
roles
Owner : ownerSlot
Minter : minterSlot
Pauser : pauserSlot
storage
ownerSlot : Address := slot 0
minterSlot : Address := slot 1
pauserSlot : Address := slot 2
@[requires Minter]
function mint (to : Address, amount : Uint256) : Unit := do
...
@[requires Owner]
function setMinter (newMinter : Address) : Unit := do
setStorageAddr minterSlot newMinter
-- No role annotation = public function
function balanceOf (addr : Address) : Uint256 := do
...
```
What `@[requires Role]` Generates
1. Runtime require check (injected at function entry):
```lean
require (sender == getStorageAddr ownerSlot) "Not owner"
```
Compiles to Yul:
```yul
if iszero(eq(caller(), sload(ownerSlot))) { revert(...) }
```
2. Access control theorem (auto-generated):
```lean
theorem setMinter_requires_owner :
∀ s s' sender newMinter, setMinter_spec newMinter sender s s' →
sender = s.storage ownerSlot := by ...
```
This theorem is auto-provable because the macro inserted the require. Its value:
Extensions
Implementation Plan
Estimated effort: 2–3 weeks
Feature 3: Explicit `unsafe` Blocks (P1)
Problem
`delegatecall`, inline assembly, raw storage access, and external calls are all available everywhere in Solidity with no distinction from safe code. There's no way to grep for "all the dangerous operations in this contract."
What Verity Already Has
Design
```
function upgradeImplementation (newImpl : Address) : Unit := do
@[requires Owner]
let currentImpl ← getStorage implSlot
unsafe "delegatecall to new implementation" do
delegatecall gas() newImpl 0 0 0 0
-- Back to safe code
setStorage implSlot newImpl
```
The `unsafe` keyword:
Implementation Plan
Estimated effort: 2–3 weeks
Shared Pattern: Escalation Ladder
All three features follow the same safety philosophy:
```
SAFE BY DEFAULT (compiler enforces)
↓ opt-out via:
LEAN PROOF (machine-checked, zero trust)
↓ if proof not feasible:
KNOWN-SAFE GUARD (nonReentrant, require, etc.)
↓ if guard not applicable:
EXPLICIT ANNOTATION (trust surface marker in --trust-report)
```
This gives developers clear escape hatches while keeping the default behavior strictly safe.
Execution Order
Related Issues