Skip to content

Commit 8652f5a

Browse files
committed
Add sstoreIfEq helper and clearIfEq example
1 parent 748b0da commit 8652f5a

9 files changed

Lines changed: 144 additions & 11 deletions

File tree

STATUS.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -19,7 +19,7 @@ Last updated: 2026-02-10
1919
- Decide whether to guard old-state specs with `from ≠ to` or adopt sequential reads by default.
2020
- Supply accounting abstraction (list vs set/dedup semantics).
2121
- EDSL ergonomics: add helpers, notations, and a minimal stdlib for common patterns.
22-
- Iteration: add `requireZero` helper and a tiny `initToOne` example + Foundry test.
22+
- Iteration: add `sstoreIfEq` helper and a tiny `clearIfEq` example + Foundry test.
2323

2424
## Recently Done
2525
- Added `requireZero` helper and an `initToOne` example + Foundry test.

docs/research-log.md

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,11 @@
11
# Research Log
22

33
## 2026-02-10
4+
- Added `sstoreIfEq` stdlib helper for guarded stores on slot equality.
5+
- Refactored `compareAndSwap` to use `sstoreIfEq`.
6+
- Added `clearIfEq` example (clear slot when it matches expected) with SpecR + proofs.
7+
- Added Foundry test for `clearIfEq`.
8+
- Updated compiler entries + direct EVM asm for `clearIfEq`.
49
- Added `requireZero` stdlib helper for zero guards.
510
- Refactored `initOnce` to use `requireZero`.
611
- Added `initToOne` example (initialize slot to `1` when empty) with SpecR + proofs.

research/lean_only_proto/DumbContracts/Compiler.lean

Lines changed: 16 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -181,6 +181,19 @@ def exampleEntry7 : EntryPoint :=
181181
selector := 0xc74962fa
182182
returns := false }
183183

184+
def exampleEntry21 : EntryPoint :=
185+
{ name := "clearIfEq"
186+
args := ["slot", "expected"]
187+
body :=
188+
Lang.Stmt.let_ "current" (Lang.Expr.sload (Lang.Expr.var "slot"))
189+
(Lang.Stmt.if_
190+
(Lang.Expr.eq (Lang.Expr.var "current") (Lang.Expr.var "expected"))
191+
(Lang.Stmt.sstore (Lang.Expr.var "slot") (Lang.Expr.lit 0))
192+
Lang.Stmt.revert)
193+
-- clearIfEq(uint256,uint256) -> 0xb41535b4
194+
selector := 0xb41535b4
195+
returns := false }
196+
184197
def exampleEntry12 : EntryPoint :=
185198
{ name := "initOnce"
186199
args := ["slot", "value"]
@@ -333,9 +346,9 @@ def exampleEntry14 : EntryPoint :=
333346

334347
def exampleEntries : List EntryPoint :=
335348
[exampleEntry, exampleEntry2, exampleEntry3, exampleEntry4, exampleEntry5, exampleEntry15,
336-
exampleEntry16, exampleEntry6, exampleEntry7, exampleEntry12, exampleEntry20, exampleEntry8,
337-
exampleEntry9, exampleEntry19, exampleEntry10, exampleEntry11, exampleEntry17, exampleEntry18,
338-
exampleEntry13, exampleEntry14]
349+
exampleEntry16, exampleEntry6, exampleEntry7, exampleEntry21, exampleEntry12, exampleEntry20,
350+
exampleEntry8, exampleEntry9, exampleEntry19, exampleEntry10, exampleEntry11, exampleEntry17,
351+
exampleEntry18, exampleEntry13, exampleEntry14]
339352

340353
def healthEntrySet : EntryPoint :=
341354
{ name := "setRisk"

research/lean_only_proto/DumbContracts/EvmAsm.lean

Lines changed: 41 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -54,6 +54,11 @@ def directProgramAsm : List String :=
5454
, "PUSH2 tag_compareAndSwap"
5555
, "JUMPI"
5656
, "DUP1"
57+
, "PUSH4 0xb41535b4"
58+
, "EQ"
59+
, "PUSH2 tag_clearIfEq"
60+
, "JUMPI"
61+
, "DUP1"
5762
, "PUSH4 0xd3b9b05a"
5863
, "EQ"
5964
, "PUSH2 tag_initOnce"
@@ -230,6 +235,16 @@ def directProgramAsm : List String :=
230235
, "JUMP"
231236
, "ret_initOnce:"
232237
, "STOP"
238+
, "tag_clearIfEq:"
239+
, "PUSH2 ret_clearIfEq"
240+
, "PUSH1 0x24"
241+
, "CALLDATALOAD"
242+
, "PUSH1 0x04"
243+
, "CALLDATALOAD"
244+
, "PUSH2 fn_clearIfEq"
245+
, "JUMP"
246+
, "ret_clearIfEq:"
247+
, "STOP"
233248
, "tag_compareAndSwap:"
234249
, "PUSH2 ret_compareAndSwap"
235250
, "PUSH1 0x44"
@@ -509,6 +524,32 @@ def directProgramAsm : List String :=
509524
, "DUP1"
510525
, "PUSH2 compareAndSwap_check"
511526
, "JUMP"
527+
, "fn_clearIfEq:"
528+
, "DUP1"
529+
, "SLOAD"
530+
, "SWAP1"
531+
, "DUP3"
532+
, "DUP3"
533+
, "EQ"
534+
, "PUSH2 clearIfEq_ok"
535+
, "JUMPI"
536+
, "clearIfEq_check:"
537+
, "POP"
538+
, "SUB"
539+
, "PUSH2 clearIfEq_revert"
540+
, "JUMPI"
541+
, "JUMP"
542+
, "clearIfEq_revert:"
543+
, "PUSH0"
544+
, "DUP1"
545+
, "REVERT"
546+
, "clearIfEq_ok:"
547+
, "PUSH0"
548+
, "SWAP1"
549+
, "SSTORE"
550+
, "PUSH0"
551+
, "PUSH2 clearIfEq_check"
552+
, "JUMP"
512553
, "fn_initOnce:"
513554
, "PUSH0"
514555
, "SWAP2"

research/lean_only_proto/DumbContracts/Examples.lean

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,7 @@ import DumbContracts.Examples.UpdateMax
88
import DumbContracts.Examples.UpdateMin
99
import DumbContracts.Examples.NonZeroStore
1010
import DumbContracts.Examples.CompareAndSwap
11+
import DumbContracts.Examples.ClearIfEq
1112
import DumbContracts.Examples.InitOnce
1213
import DumbContracts.Examples.InitToOne
1314
import DumbContracts.Examples.SetIfGreater
Lines changed: 42 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,42 @@
1+
import DumbContracts.Lang
2+
import DumbContracts.Semantics
3+
import DumbContracts.Stdlib
4+
5+
namespace DumbContracts.Examples
6+
7+
open DumbContracts.Lang
8+
open DumbContracts.Semantics
9+
open DumbContracts
10+
open DumbContracts.Std
11+
12+
/-- Clear a slot (store 0) only if it matches an expected value. -/
13+
14+
def clearIfEqFun : Fun :=
15+
{ name := "clearIfEq"
16+
args := ["slot", "expected"]
17+
body := sstoreIfEq (v "slot") (v "expected") (n 0)
18+
ret := none }
19+
20+
def clearIfEqSpecR (slot expected : Nat) : SpecR Store :=
21+
{ requires := fun s => s slot = expected
22+
ensures := fun s s' => s' = updateStore s slot 0
23+
reverts := fun s => s slot != expected }
24+
25+
theorem clearIfEq_meets_specR_ok (s : Store) (slot expected : Nat) :
26+
(clearIfEqSpecR slot expected).requires s ->
27+
(match execFun clearIfEqFun [slot, expected] s [] with
28+
| ExecResult.ok _ s' => (clearIfEqSpecR slot expected).ensures s s'
29+
| _ => False) := by
30+
intro hreq
31+
have hmatch : s slot = expected := by exact hreq
32+
simp [clearIfEqSpecR, clearIfEqFun, sstoreIfEq, requireEq, eq, require, execFun, execStmt,
33+
evalExpr, bindArgs, emptyEnv, updateEnv, updateStore, letSload, hmatch]
34+
35+
theorem clearIfEq_meets_specR_reverts (s : Store) (slot expected : Nat) :
36+
(clearIfEqSpecR slot expected).reverts s ->
37+
execFun clearIfEqFun [slot, expected] s [] = ExecResult.reverted := by
38+
intro hrev
39+
simp [clearIfEqSpecR, clearIfEqFun, sstoreIfEq, requireEq, eq, require, execFun, execStmt,
40+
evalExpr, bindArgs, emptyEnv, updateEnv, updateStore, letSload, hrev]
41+
42+
end DumbContracts.Examples

research/lean_only_proto/DumbContracts/Examples/CompareAndSwap.lean

Lines changed: 5 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -14,9 +14,7 @@ open DumbContracts.Std
1414
def compareAndSwapFun : Fun :=
1515
{ name := "compareAndSwap"
1616
args := ["slot", "expected", "value"]
17-
body :=
18-
letSload "current" (v "slot")
19-
(requireEq (v "current") (v "expected") (sstoreVar "slot" (v "value")))
17+
body := sstoreIfEq (v "slot") (v "expected") (v "value")
2018
ret := none }
2119

2220
def compareAndSwapSpecR (slot expected value : Nat) : SpecR Store :=
@@ -31,14 +29,14 @@ theorem compareAndSwap_meets_specR_ok (s : Store) (slot expected value : Nat) :
3129
| _ => False) := by
3230
intro hreq
3331
have hmatch : s slot = expected := by exact hreq
34-
simp [compareAndSwapSpecR, compareAndSwapFun, requireEq, eq, require, execFun, execStmt,
35-
evalExpr, bindArgs, emptyEnv, updateEnv, updateStore, letSload, hmatch]
32+
simp [compareAndSwapSpecR, compareAndSwapFun, sstoreIfEq, requireEq, eq, require, execFun,
33+
execStmt, evalExpr, bindArgs, emptyEnv, updateEnv, updateStore, letSload, hmatch]
3634

3735
theorem compareAndSwap_meets_specR_reverts (s : Store) (slot expected value : Nat) :
3836
(compareAndSwapSpecR slot expected value).reverts s ->
3937
execFun compareAndSwapFun [slot, expected, value] s [] = ExecResult.reverted := by
4038
intro hrev
41-
simp [compareAndSwapSpecR, compareAndSwapFun, requireEq, eq, require, execFun, execStmt,
42-
evalExpr, bindArgs, emptyEnv, updateEnv, updateStore, letSload, hrev]
39+
simp [compareAndSwapSpecR, compareAndSwapFun, sstoreIfEq, requireEq, eq, require, execFun,
40+
execStmt, evalExpr, bindArgs, emptyEnv, updateEnv, updateStore, letSload, hrev]
4341

4442
end DumbContracts.Examples

research/lean_only_proto/DumbContracts/Stdlib.lean

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -60,6 +60,9 @@ def requireZero (value : Expr) (body : Stmt) : Stmt :=
6060
def letSload (name : String) (slot : Expr) (body : Stmt) : Stmt :=
6161
Stmt.let_ name (Expr.sload slot) body
6262

63+
def sstoreIfEq (slot expected value : Expr) : Stmt :=
64+
letSload "current" slot (requireEq (Expr.var "current") expected (Stmt.sstore slot value))
65+
6366
def sstoreAdd (slot delta : Expr) : Stmt :=
6467
Stmt.sstore slot (Expr.add (Expr.sload slot) delta)
6568

Lines changed: 30 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,30 @@
1+
// SPDX-License-Identifier: MIT
2+
pragma solidity ^0.8.20;
3+
4+
import "./GeneratedBase.t.sol";
5+
6+
contract GeneratedClearIfEqTest is GeneratedBase {
7+
function testClearIfEq() public {
8+
bytes memory creation = _readHexFile("out/example.creation.bin");
9+
address deployed = _deploy(creation);
10+
11+
bytes4 selGet = 0x7eba7ba6;
12+
bytes4 selSet = 0xf2c298be;
13+
bytes4 selClear = 0xb41535b4;
14+
15+
(bool ok,) = deployed.call(abi.encodeWithSelector(selSet, 22, 55));
16+
require(ok, "setSlot failed");
17+
18+
(ok,) = deployed.call(abi.encodeWithSelector(selClear, 22, 55));
19+
require(ok, "clearIfEq should succeed");
20+
21+
bytes memory data;
22+
(ok, data) = deployed.call(abi.encodeWithSelector(selGet, 22));
23+
require(ok, "getSlot failed");
24+
uint256 val = abi.decode(data, (uint256));
25+
require(val == 0, "unexpected clearIfEq value");
26+
27+
(ok,) = deployed.call(abi.encodeWithSelector(selClear, 22, 7));
28+
require(!ok, "clearIfEq should revert on mismatch");
29+
}
30+
}

0 commit comments

Comments
 (0)