Skip to content

Commit f8394f3

Browse files
committed
Add sstoreIfLt helper and capSlot example
1 parent 8bec91b commit f8394f3

9 files changed

Lines changed: 167 additions & 24 deletions

File tree

STATUS.md

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -19,9 +19,11 @@ 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 `sstoreIfLt` helper plus a capped-slot example to avoid redundant writes.
2223

2324

2425
## Recently Done
26+
- Added `sstoreIfLt` helper, refactored `updateMin`, and added `capSlot` example + Foundry test.
2527
- Added `sstoreIfZero` helper and an `initDouble` example + Foundry test.
2628
- Added `requireZero` helper and an `initToOne` example + Foundry test.
2729
- Lean -> Yul pipeline with runtime + creation bytecode artifacts.

docs/research-log.md

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -118,3 +118,10 @@
118118
- Updated compiler entries + direct EVM asm wiring for `initDouble`.
119119
- `end_to_end` initially failed due to direct EVM asm mismatch in `initDouble`; fixed by aligning the stack order (`swap1` prelude + `swap1` before `sstore`) with solc output.
120120
- Ran `PATH=/opt/lean-4.27.0/bin:$PATH lake build`, `./scripts/end_to_end.sh`, and `./scripts/foundry_test_generated.sh` (all green).
121+
- Added `sstoreIfLt` stdlib helper to avoid redundant stores when capping a slot.
122+
- Refactored `updateMin` to use `sstoreIfLt` (skip store when the slot is already below the cap).
123+
- Added `capSlot` example (cap a slot to a maximum without reverting) with Spec + proof.
124+
- Added Foundry test for `capSlot`.
125+
- Updated compiler entries + direct EVM asm wiring for `updateMin` and `capSlot`.
126+
- `end_to_end` initially failed due to direct EVM asm block layout for `capSlot`/`updateMin`; fixed by matching solc's `jump(tag_out)` shape and using `PUSH0` for zero.
127+
- Ran `PATH=/opt/lean-4.27.0/bin:$PATH lake build`, `./scripts/end_to_end.sh`, and `./scripts/foundry_test_generated.sh` (all green).

research/lean_only_proto/DumbContracts/Compiler.lean

Lines changed: 19 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -150,13 +150,26 @@ def exampleEntry16 : EntryPoint :=
150150
body :=
151151
Lang.Stmt.let_ "current" (Lang.Expr.sload (Lang.Expr.var "slot"))
152152
(Lang.Stmt.if_
153-
(Lang.Expr.lt (Lang.Expr.var "current") (Lang.Expr.var "value"))
154-
(Lang.Stmt.sstore (Lang.Expr.var "slot") (Lang.Expr.var "current"))
155-
(Lang.Stmt.sstore (Lang.Expr.var "slot") (Lang.Expr.var "value")))
153+
(Lang.Expr.lt (Lang.Expr.var "value") (Lang.Expr.var "current"))
154+
(Lang.Stmt.sstore (Lang.Expr.var "slot") (Lang.Expr.var "value"))
155+
Lang.Stmt.skip)
156156
-- updateMin(uint256,uint256) -> 0x5c34a245
157157
selector := 0x5c34a245
158158
returns := false }
159159

160+
def exampleEntry24 : EntryPoint :=
161+
{ name := "capSlot"
162+
args := ["slot", "cap"]
163+
body :=
164+
Lang.Stmt.let_ "current" (Lang.Expr.sload (Lang.Expr.var "slot"))
165+
(Lang.Stmt.if_
166+
(Lang.Expr.lt (Lang.Expr.var "cap") (Lang.Expr.var "current"))
167+
(Lang.Stmt.sstore (Lang.Expr.var "slot") (Lang.Expr.var "cap"))
168+
Lang.Stmt.skip)
169+
-- capSlot(uint256,uint256) -> 0x7fdb8622
170+
selector := 0x7fdb8622
171+
returns := false }
172+
160173
def exampleEntry6 : EntryPoint :=
161174
{ name := "setNonZero"
162175
args := ["slot", "value"]
@@ -377,9 +390,9 @@ def exampleEntry14 : EntryPoint :=
377390

378391
def exampleEntries : List EntryPoint :=
379392
[exampleEntry, exampleEntry2, exampleEntry3, exampleEntry4, exampleEntry5, exampleEntry15,
380-
exampleEntry16, exampleEntry6, exampleEntry7, exampleEntry21, exampleEntry12, exampleEntry20,
381-
exampleEntry23, exampleEntry8, exampleEntry9, exampleEntry19, exampleEntry10, exampleEntry11,
382-
exampleEntry17, exampleEntry18, exampleEntry13, exampleEntry22, exampleEntry14]
393+
exampleEntry16, exampleEntry24, exampleEntry6, exampleEntry7, exampleEntry21, exampleEntry12,
394+
exampleEntry20, exampleEntry23, exampleEntry8, exampleEntry9, exampleEntry19, exampleEntry10,
395+
exampleEntry11, exampleEntry17, exampleEntry18, exampleEntry13, exampleEntry22, exampleEntry14]
383396

384397
def healthEntrySet : EntryPoint :=
385398
{ name := "setRisk"

research/lean_only_proto/DumbContracts/EvmAsm.lean

Lines changed: 56 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -44,6 +44,11 @@ def directProgramAsm : List String :=
4444
, "PUSH2 tag_updateMin"
4545
, "JUMPI"
4646
, "DUP1"
47+
, "PUSH4 0x7fdb8622"
48+
, "EQ"
49+
, "PUSH2 tag_capSlot"
50+
, "JUMPI"
51+
, "DUP1"
4752
, "PUSH4 0xac1f1f67"
4853
, "EQ"
4954
, "PUSH2 tag_setNonZero"
@@ -299,6 +304,16 @@ def directProgramAsm : List String :=
299304
, "JUMP"
300305
, "ret_setNonZero:"
301306
, "STOP"
307+
, "tag_capSlot:"
308+
, "PUSH2 ret_capSlot"
309+
, "PUSH1 0x24"
310+
, "CALLDATALOAD"
311+
, "PUSH1 0x04"
312+
, "CALLDATALOAD"
313+
, "PUSH2 fn_capSlot"
314+
, "JUMP"
315+
, "ret_capSlot:"
316+
, "STOP"
302317
, "tag_updateMin:"
303318
, "PUSH2 ret_updateMin"
304319
, "PUSH1 0x24"
@@ -472,34 +487,62 @@ def directProgramAsm : List String :=
472487
, "PUSH2 updateMax_check"
473488
, "JUMP"
474489
, "fn_updateMin:"
475-
, "DUP2"
476-
, "DUP2"
490+
, "SWAP1"
491+
, "DUP1"
492+
, "DUP3"
477493
, "SLOAD"
478-
, "DUP2"
479-
, "DUP2"
494+
, "SWAP3"
495+
, "DUP4"
496+
, "DUP3"
480497
, "LT"
481498
, "PUSH2 updateMin_then"
482499
, "JUMPI"
483500
, "updateMin_check:"
501+
, "POP"
502+
, "POP"
484503
, "LT"
485504
, "ISZERO"
486-
, "PUSH2 updateMin_else"
505+
, "PUSH2 updateMin_skip"
487506
, "JUMPI"
488-
, "updateMin_join:"
489-
, "POP"
490-
, "POP"
507+
, "updateMin_out:"
491508
, "JUMP"
492-
, "updateMin_else:"
509+
, "updateMin_skip:"
510+
, "PUSH2 updateMin_out"
511+
, "JUMP"
512+
, "updateMin_then:"
493513
, "SSTORE"
494-
, "PUSH0"
495514
, "DUP1"
496-
, "PUSH2 updateMin_join"
515+
, "PUSH0"
516+
, "PUSH2 updateMin_check"
497517
, "JUMP"
498-
, "updateMin_then:"
518+
, "fn_capSlot:"
519+
, "SWAP1"
499520
, "DUP1"
521+
, "DUP3"
522+
, "SLOAD"
523+
, "SWAP3"
500524
, "DUP4"
525+
, "DUP3"
526+
, "LT"
527+
, "PUSH2 capSlot_then"
528+
, "JUMPI"
529+
, "capSlot_check:"
530+
, "POP"
531+
, "POP"
532+
, "LT"
533+
, "ISZERO"
534+
, "PUSH2 capSlot_skip"
535+
, "JUMPI"
536+
, "capSlot_out:"
537+
, "JUMP"
538+
, "capSlot_skip:"
539+
, "PUSH2 capSlot_out"
540+
, "JUMP"
541+
, "capSlot_then:"
501542
, "SSTORE"
502-
, "PUSH2 updateMin_check"
543+
, "DUP1"
544+
, "PUSH0"
545+
, "PUSH2 capSlot_check"
503546
, "JUMP"
504547
, "fn_setNonZero:"
505548
, "SWAP1"

research/lean_only_proto/DumbContracts/Examples.lean

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@ import DumbContracts.Examples.TokenTransfer
66
import DumbContracts.Examples.MaxStore
77
import DumbContracts.Examples.UpdateMax
88
import DumbContracts.Examples.UpdateMin
9+
import DumbContracts.Examples.CapSlot
910
import DumbContracts.Examples.NonZeroStore
1011
import DumbContracts.Examples.CompareAndSwap
1112
import DumbContracts.Examples.ClearIfEq
Lines changed: 37 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,37 @@
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+
/-- Cap a slot to a maximum value without reverting. -/
13+
14+
def capSlotFun : Fun :=
15+
{ name := "capSlot"
16+
args := ["slot", "cap"]
17+
body := sstoreIfLt (v "slot") (v "cap")
18+
ret := none }
19+
20+
def capSlotSpec (slot cap : Nat) : Spec Store :=
21+
{ requires := fun _ => True
22+
ensures := fun s s' =>
23+
s' = updateStore s slot (if s slot > cap then cap else s slot) }
24+
25+
theorem capSlot_meets_spec (s : Store) (slot cap : Nat) :
26+
(capSlotSpec slot cap).requires s ->
27+
(match execFun capSlotFun [slot, cap] s [] with
28+
| ExecResult.ok _ s' => (capSlotSpec slot cap).ensures s s'
29+
| _ => False) := by
30+
intro _hreq
31+
by_cases h : cap < s slot
32+
· simp [capSlotFun, capSlotSpec, sstoreIfLt, letSload, v, execFun, execStmt, evalExpr,
33+
bindArgs, emptyEnv, updateEnv, updateStore, h]
34+
· simp [capSlotFun, capSlotSpec, sstoreIfLt, letSload, v, execFun, execStmt, evalExpr,
35+
bindArgs, emptyEnv, updateEnv, updateStore, h]
36+
37+
end DumbContracts.Examples

research/lean_only_proto/DumbContracts/Examples/UpdateMin.lean

Lines changed: 4 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -15,8 +15,7 @@ def updateMinFun : Fun :=
1515
{ name := "updateMin"
1616
args := ["slot", "value"]
1717
body :=
18-
letSload "current" (v "slot")
19-
(sstoreMin (v "slot") (v "current") (v "value"))
18+
sstoreIfLt (v "slot") (v "value")
2019
ret := none }
2120

2221
def updateMinSpec (slot value : Nat) : Spec Store :=
@@ -29,10 +28,10 @@ theorem updateMin_meets_spec (s : Store) (slot value : Nat) :
2928
| ExecResult.ok _ s' => (updateMinSpec slot value).ensures s s'
3029
| _ => False) := by
3130
intro _hreq
32-
by_cases h : s slot < value
33-
· simp [updateMinFun, updateMinSpec, letSload, sstoreMin, v, execFun, execStmt, evalExpr,
31+
by_cases h : value < s slot
32+
· simp [updateMinFun, updateMinSpec, sstoreIfLt, letSload, v, execFun, execStmt, evalExpr,
3433
bindArgs, emptyEnv, updateEnv, updateStore, h]
35-
· simp [updateMinFun, updateMinSpec, letSload, sstoreMin, v, execFun, execStmt, evalExpr,
34+
· simp [updateMinFun, updateMinSpec, sstoreIfLt, letSload, v, execFun, execStmt, evalExpr,
3635
bindArgs, emptyEnv, updateEnv, updateStore, h]
3736

3837
end DumbContracts.Examples

research/lean_only_proto/DumbContracts/Stdlib.lean

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -69,6 +69,13 @@ def sstoreIfEq (slot expected value : Expr) : Stmt :=
6969
def sstoreIfZero (slot value : Expr) : Stmt :=
7070
letSload "current" slot (requireZero (Expr.var "current") (Stmt.sstore slot value))
7171

72+
def sstoreIfLt (slot value : Expr) : Stmt :=
73+
letSload "current" slot
74+
(Stmt.if_
75+
(Expr.lt value (Expr.var "current"))
76+
(Stmt.sstore slot value)
77+
Stmt.skip)
78+
7279
def sstoreAdd (slot delta : Expr) : Stmt :=
7380
Stmt.sstore slot (Expr.add (Expr.sload slot) delta)
7481

Lines changed: 34 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,34 @@
1+
// SPDX-License-Identifier: MIT
2+
pragma solidity ^0.8.20;
3+
4+
import "./GeneratedBase.t.sol";
5+
6+
contract GeneratedCapSlotTest is GeneratedBase {
7+
function testCapSlot() public {
8+
bytes memory creation = _readHexFile("out/example.creation.bin");
9+
address deployed = _deploy(creation);
10+
11+
bytes4 selSetSlot = 0xf2c298be;
12+
bytes4 selCapSlot = 0x7fdb8622;
13+
bytes4 selGet = 0x7eba7ba6;
14+
15+
(bool ok,) = deployed.call(abi.encodeWithSelector(selSetSlot, 5, 20));
16+
require(ok, "setSlot failed");
17+
18+
(ok,) = deployed.call(abi.encodeWithSelector(selCapSlot, 5, 15));
19+
require(ok, "capSlot failed (cap 15)");
20+
21+
bytes memory data;
22+
(ok, data) = deployed.call(abi.encodeWithSelector(selGet, 5));
23+
require(ok, "getSlot failed (slot 5)");
24+
uint256 val = abi.decode(data, (uint256));
25+
require(val == 15, "capSlot did not lower value");
26+
27+
(ok,) = deployed.call(abi.encodeWithSelector(selCapSlot, 5, 30));
28+
require(ok, "capSlot failed (cap 30)");
29+
(ok, data) = deployed.call(abi.encodeWithSelector(selGet, 5));
30+
require(ok, "getSlot failed (slot 5, after cap 30)");
31+
val = abi.decode(data, (uint256));
32+
require(val == 15, "capSlot should not raise value");
33+
}
34+
}

0 commit comments

Comments
 (0)