Skip to content

Commit 9c078a7

Browse files
committed
Add sstoreInc helper and bumpIfNonZero example
1 parent 30c2a2c commit 9c078a7

9 files changed

Lines changed: 148 additions & 4 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 `requireAnd` helper plus a `setIfNonZeroAndLess` example to reduce nested guard boilerplate.
22+
- Iteration: add `sstoreInc` helper plus a `bumpIfNonZero` example to reduce increment boilerplate with guards.
2323

2424
## Recently Done
2525
- Lean -> Yul pipeline with runtime + creation bytecode artifacts.

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 `sstoreInc` stdlib helper to increment a slot without repeating `add` boilerplate.
5+
- Refactored `bumpSlot` to use `sstoreInc`.
6+
- Added `bumpIfNonZero` example (guarded increment when slot is nonzero) with SpecR + proofs.
7+
- Added Foundry test for `bumpIfNonZero`.
8+
- Updated compiler entries + direct EVM asm for `bumpIfNonZero`.
49
- Added `requireAnd` stdlib helper to combine guard conditions cleanly.
510
- Refactored `setIfBetween` to use `requireAnd` instead of nested guards.
611
- Added `setIfNonZeroAndLess` example (nonzero + max guard) with SpecR + proofs.

research/lean_only_proto/DumbContracts/Compiler.lean

Lines changed: 17 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -232,9 +232,25 @@ def exampleEntry13 : EntryPoint :=
232232
selector := 0x8ba43d8f
233233
returns := false }
234234

235+
def exampleEntry14 : EntryPoint :=
236+
{ name := "bumpIfNonZero"
237+
args := ["slot"]
238+
body :=
239+
Lang.Stmt.if_
240+
(Lang.Expr.not
241+
(Lang.Expr.eq (Lang.Expr.sload (Lang.Expr.var "slot")) (Lang.Expr.lit 0)))
242+
(Lang.Stmt.sstore
243+
(Lang.Expr.var "slot")
244+
(Lang.Expr.add (Lang.Expr.sload (Lang.Expr.var "slot")) (Lang.Expr.lit 1)))
245+
Lang.Stmt.revert
246+
-- bumpIfNonZero(uint256) -> 0xc2311456
247+
selector := 0xc2311456
248+
returns := false }
249+
235250
def exampleEntries : List EntryPoint :=
236251
[exampleEntry, exampleEntry2, exampleEntry3, exampleEntry4, exampleEntry5, exampleEntry6, exampleEntry7,
237-
exampleEntry12, exampleEntry8, exampleEntry9, exampleEntry10, exampleEntry11, exampleEntry13]
252+
exampleEntry12, exampleEntry8, exampleEntry9, exampleEntry10, exampleEntry11, exampleEntry13,
253+
exampleEntry14]
238254

239255
def healthEntrySet : EntryPoint :=
240256
{ name := "setRisk"

research/lean_only_proto/DumbContracts/EvmAsm.lean

Lines changed: 41 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -68,13 +68,26 @@ def directProgramAsm : List String :=
6868
, "EQ"
6969
, "PUSH2 tag_setIfBetween"
7070
, "JUMPI"
71+
, "DUP1"
7172
, "PUSH4 0x8ba43d8f"
7273
, "EQ"
7374
, "PUSH2 tag_setIfNonZeroAndLess"
7475
, "JUMPI"
76+
, "PUSH4 0xc2311456"
77+
, "EQ"
78+
, "PUSH2 tag_bumpIfNonZero"
79+
, "JUMPI"
7580
, "PUSH0"
7681
, "DUP1"
7782
, "REVERT"
83+
, "tag_bumpIfNonZero:"
84+
, "PUSH2 ret_bumpIfNonZero"
85+
, "PUSH1 0x04"
86+
, "CALLDATALOAD"
87+
, "PUSH2 fn_bumpIfNonZero"
88+
, "JUMP"
89+
, "ret_bumpIfNonZero:"
90+
, "STOP"
7891
, "tag_setIfNonZeroAndLess:"
7992
, "PUSH2 ret_setIfNonZeroAndLess"
8093
, "PUSH1 0x44"
@@ -518,6 +531,34 @@ def directProgramAsm : List String :=
518531
, "PUSH0"
519532
, "PUSH2 setIfNonZeroAndLess_check"
520533
, "JUMP"
534+
, "fn_bumpIfNonZero:"
535+
, "PUSH0"
536+
, "SWAP1"
537+
, "DUP2"
538+
, "DUP2"
539+
, "SLOAD"
540+
, "SUB"
541+
, "PUSH2 bumpIfNonZero_ok"
542+
, "JUMPI"
543+
, "bumpIfNonZero_check:"
544+
, "SLOAD"
545+
, "EQ"
546+
, "PUSH2 bumpIfNonZero_revert"
547+
, "JUMPI"
548+
, "JUMP"
549+
, "bumpIfNonZero_revert:"
550+
, "PUSH0"
551+
, "DUP1"
552+
, "REVERT"
553+
, "bumpIfNonZero_ok:"
554+
, "PUSH1 0x01"
555+
, "DUP2"
556+
, "SLOAD"
557+
, "ADD"
558+
, "DUP2"
559+
, "SSTORE"
560+
, "PUSH2 bumpIfNonZero_check"
561+
, "JUMP"
521562
]
522563

523564
def pretty (lines : List String) : String :=

research/lean_only_proto/DumbContracts/Examples.lean

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -12,3 +12,4 @@ import DumbContracts.Examples.SetIfLess
1212
import DumbContracts.Examples.SetIfBetween
1313
import DumbContracts.Examples.SetIfNonZeroAndLess
1414
import DumbContracts.Examples.BumpSlot
15+
import DumbContracts.Examples.BumpIfNonZero
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+
/-- Increment a slot by one, but only if it is already non-zero. -/
13+
14+
def bumpIfNonZeroFun : Fun :=
15+
{ name := "bumpIfNonZero"
16+
args := ["slot"]
17+
body := requireNonZero (Expr.sload (v "slot")) (sstoreInc (v "slot"))
18+
ret := none }
19+
20+
def bumpIfNonZeroSpecR (slot : Nat) : SpecR Store :=
21+
{ requires := fun s => s slot != 0
22+
ensures := fun s s' => s' = updateStore s slot (s slot + 1)
23+
reverts := fun s => s slot = 0 }
24+
25+
theorem bumpIfNonZero_meets_specR_ok (s : Store) (slot : Nat) :
26+
(bumpIfNonZeroSpecR slot).requires s ->
27+
(match execFun bumpIfNonZeroFun [slot] s [] with
28+
| ExecResult.ok _ s' => (bumpIfNonZeroSpecR slot).ensures s s'
29+
| _ => False) := by
30+
intro hreq
31+
have hnonzero : s slot != 0 := by exact hreq
32+
simp [bumpIfNonZeroSpecR, bumpIfNonZeroFun, requireNonZero, requireNeq, neq, eq, require, sstoreInc,
33+
sstoreAdd, execFun, execStmt, evalExpr, bindArgs, emptyEnv, updateEnv, updateStore, v, n, hnonzero]
34+
35+
theorem bumpIfNonZero_meets_specR_reverts (s : Store) (slot : Nat) :
36+
(bumpIfNonZeroSpecR slot).reverts s ->
37+
execFun bumpIfNonZeroFun [slot] s [] = ExecResult.reverted := by
38+
intro hrev
39+
simp [bumpIfNonZeroSpecR, bumpIfNonZeroFun, requireNonZero, requireNeq, neq, eq, require, sstoreInc,
40+
sstoreAdd, execFun, execStmt, evalExpr, bindArgs, emptyEnv, updateEnv, updateStore, v, n, hrev]
41+
42+
end DumbContracts.Examples

research/lean_only_proto/DumbContracts/Examples/BumpSlot.lean

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -14,7 +14,7 @@ open DumbContracts.Std
1414
def bumpSlotFun : Fun :=
1515
{ name := "bumpSlot"
1616
args := ["slot"]
17-
body := sstoreAdd (v "slot") (n 1)
17+
body := sstoreInc (v "slot")
1818
ret := none }
1919

2020
def bumpSlotSpec (slot : Nat) : Spec Store :=
@@ -27,7 +27,7 @@ theorem bumpSlot_meets_spec (s : Store) (slot : Nat) :
2727
| ExecResult.ok _ s' => (bumpSlotSpec slot).ensures s s'
2828
| _ => False) := by
2929
intro _hreq
30-
simp [bumpSlotSpec, bumpSlotFun, sstoreAdd, execFun, execStmt, evalExpr,
30+
simp [bumpSlotSpec, bumpSlotFun, sstoreInc, sstoreAdd, execFun, execStmt, evalExpr,
3131
bindArgs, emptyEnv, updateEnv, updateStore, v, n]
3232

3333
end DumbContracts.Examples

research/lean_only_proto/DumbContracts/Stdlib.lean

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -54,6 +54,9 @@ def letSload (name : String) (slot : Expr) (body : Stmt) : Stmt :=
5454
def sstoreAdd (slot delta : Expr) : Stmt :=
5555
Stmt.sstore slot (Expr.add (Expr.sload slot) delta)
5656

57+
def sstoreInc (slot : Expr) : Stmt :=
58+
sstoreAdd slot (Expr.lit 1)
59+
5760
def sloadSlot (slot : Nat) : Expr :=
5861
Expr.sload (Expr.lit slot)
5962

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

0 commit comments

Comments
 (0)