Skip to content

Commit 5dac4af

Browse files
committed
Add sstoreIfGt helper and raiseSlot example
1 parent f8394f3 commit 5dac4af

9 files changed

Lines changed: 171 additions & 29 deletions

File tree

STATUS.md

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -19,10 +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.
22+
- Iteration: add `sstoreIfGt` helper plus a raise-slot example to avoid redundant writes.
2323

2424

2525
## Recently Done
26+
- Added `sstoreIfGt` helper, refactored `updateMax`, and added `raiseSlot` example + Foundry test.
2627
- Added `sstoreIfLt` helper, refactored `updateMin`, and added `capSlot` example + Foundry test.
2728
- Added `sstoreIfZero` helper and an `initDouble` example + Foundry test.
2829
- Added `requireZero` helper and an `initToOne` example + Foundry test.

docs/research-log.md

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

33
## 2026-02-10
4+
- Added `sstoreIfGt` stdlib helper to avoid redundant stores on monotonic increases.
5+
- Refactored `updateMax` to use `sstoreIfGt`.
6+
- Added `raiseSlot` example (raise slot to a floor without reverting) with Spec + proof.
7+
- Added Foundry test for `raiseSlot`.
8+
- Updated compiler entries + direct EVM asm wiring for `raiseSlot` and `updateMax`.
9+
- `end_to_end` initially failed due to cap/raise stub ordering; swapped the `tag_raiseSlot`/`tag_capSlot` order so jumpdest offsets match solc.
10+
- Ran `PATH=/opt/lean-4.27.0/bin:$PATH lake build`, `./scripts/end_to_end.sh`, and `./scripts/foundry_test_generated.sh` (all green).
411
- Added `requireNonZeroAndLt` stdlib helper for combined nonzero + max guards.
512
- Refactored `setIfNonZeroAndLess` to use `requireNonZeroAndLt`.
613
- Added `addIfNonZeroAndLess` example (guarded add on `delta != 0` and `delta < max`) with SpecR + proofs.

research/lean_only_proto/DumbContracts/Compiler.lean

Lines changed: 20 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -137,9 +137,9 @@ def exampleEntry15 : EntryPoint :=
137137
body :=
138138
Lang.Stmt.let_ "current" (Lang.Expr.sload (Lang.Expr.var "slot"))
139139
(Lang.Stmt.if_
140-
(Lang.Expr.gt (Lang.Expr.var "current") (Lang.Expr.var "value"))
141-
(Lang.Stmt.sstore (Lang.Expr.var "slot") (Lang.Expr.var "current"))
142-
(Lang.Stmt.sstore (Lang.Expr.var "slot") (Lang.Expr.var "value")))
140+
(Lang.Expr.gt (Lang.Expr.var "value") (Lang.Expr.var "current"))
141+
(Lang.Stmt.sstore (Lang.Expr.var "slot") (Lang.Expr.var "value"))
142+
Lang.Stmt.skip)
143143
-- updateMax(uint256,uint256) -> 0xe9cc4edd
144144
selector := 0xe9cc4edd
145145
returns := false }
@@ -170,6 +170,19 @@ def exampleEntry24 : EntryPoint :=
170170
selector := 0x7fdb8622
171171
returns := false }
172172

173+
def exampleEntry25 : EntryPoint :=
174+
{ name := "raiseSlot"
175+
args := ["slot", "floor"]
176+
body :=
177+
Lang.Stmt.let_ "current" (Lang.Expr.sload (Lang.Expr.var "slot"))
178+
(Lang.Stmt.if_
179+
(Lang.Expr.gt (Lang.Expr.var "floor") (Lang.Expr.var "current"))
180+
(Lang.Stmt.sstore (Lang.Expr.var "slot") (Lang.Expr.var "floor"))
181+
Lang.Stmt.skip)
182+
-- raiseSlot(uint256,uint256) -> 0x84c4b3e1
183+
selector := 0x84c4b3e1
184+
returns := false }
185+
173186
def exampleEntry6 : EntryPoint :=
174187
{ name := "setNonZero"
175188
args := ["slot", "value"]
@@ -390,9 +403,10 @@ def exampleEntry14 : EntryPoint :=
390403

391404
def exampleEntries : List EntryPoint :=
392405
[exampleEntry, exampleEntry2, exampleEntry3, exampleEntry4, exampleEntry5, exampleEntry15,
393-
exampleEntry16, exampleEntry24, exampleEntry6, exampleEntry7, exampleEntry21, exampleEntry12,
394-
exampleEntry20, exampleEntry23, exampleEntry8, exampleEntry9, exampleEntry19, exampleEntry10,
395-
exampleEntry11, exampleEntry17, exampleEntry18, exampleEntry13, exampleEntry22, exampleEntry14]
406+
exampleEntry16, exampleEntry24, exampleEntry25, exampleEntry6, exampleEntry7, exampleEntry21,
407+
exampleEntry12, exampleEntry20, exampleEntry23, exampleEntry8, exampleEntry9, exampleEntry19,
408+
exampleEntry10, exampleEntry11, exampleEntry17, exampleEntry18, exampleEntry13, exampleEntry22,
409+
exampleEntry14]
396410

397411
def healthEntrySet : EntryPoint :=
398412
{ name := "setRisk"

research/lean_only_proto/DumbContracts/EvmAsm.lean

Lines changed: 58 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -49,6 +49,11 @@ def directProgramAsm : List String :=
4949
, "PUSH2 tag_capSlot"
5050
, "JUMPI"
5151
, "DUP1"
52+
, "PUSH4 0x84c4b3e1"
53+
, "EQ"
54+
, "PUSH2 tag_raiseSlot"
55+
, "JUMPI"
56+
, "DUP1"
5257
, "PUSH4 0xac1f1f67"
5358
, "EQ"
5459
, "PUSH2 tag_setNonZero"
@@ -304,6 +309,16 @@ def directProgramAsm : List String :=
304309
, "JUMP"
305310
, "ret_setNonZero:"
306311
, "STOP"
312+
, "tag_raiseSlot:"
313+
, "PUSH2 ret_raiseSlot"
314+
, "PUSH1 0x24"
315+
, "CALLDATALOAD"
316+
, "PUSH1 0x04"
317+
, "CALLDATALOAD"
318+
, "PUSH2 fn_raiseSlot"
319+
, "JUMP"
320+
, "ret_raiseSlot:"
321+
, "STOP"
307322
, "tag_capSlot:"
308323
, "PUSH2 ret_capSlot"
309324
, "PUSH1 0x24"
@@ -457,33 +472,32 @@ def directProgramAsm : List String :=
457472
, "PUSH2 maxStore_check"
458473
, "JUMP"
459474
, "fn_updateMax:"
460-
, "DUP2"
461-
, "DUP2"
475+
, "SWAP1"
476+
, "DUP1"
477+
, "DUP3"
462478
, "SLOAD"
463-
, "DUP2"
464-
, "DUP2"
479+
, "SWAP3"
480+
, "DUP4"
481+
, "DUP3"
465482
, "GT"
466483
, "PUSH2 updateMax_then"
467484
, "JUMPI"
468485
, "updateMax_check:"
486+
, "POP"
487+
, "POP"
469488
, "GT"
470489
, "ISZERO"
471-
, "PUSH2 updateMax_else"
490+
, "PUSH2 updateMax_skip"
472491
, "JUMPI"
473-
, "updateMax_join:"
474-
, "POP"
475-
, "POP"
492+
, "updateMax_out:"
476493
, "JUMP"
477-
, "updateMax_else:"
478-
, "SSTORE"
479-
, "PUSH0"
480-
, "DUP1"
481-
, "PUSH2 updateMax_join"
494+
, "updateMax_skip:"
495+
, "PUSH2 updateMax_out"
482496
, "JUMP"
483497
, "updateMax_then:"
484-
, "DUP1"
485-
, "DUP4"
486498
, "SSTORE"
499+
, "DUP1"
500+
, "PUSH0"
487501
, "PUSH2 updateMax_check"
488502
, "JUMP"
489503
, "fn_updateMin:"
@@ -544,6 +558,35 @@ def directProgramAsm : List String :=
544558
, "PUSH0"
545559
, "PUSH2 capSlot_check"
546560
, "JUMP"
561+
, "fn_raiseSlot:"
562+
, "SWAP1"
563+
, "DUP1"
564+
, "DUP3"
565+
, "SLOAD"
566+
, "SWAP3"
567+
, "DUP4"
568+
, "DUP3"
569+
, "GT"
570+
, "PUSH2 raiseSlot_then"
571+
, "JUMPI"
572+
, "raiseSlot_check:"
573+
, "POP"
574+
, "POP"
575+
, "GT"
576+
, "ISZERO"
577+
, "PUSH2 raiseSlot_skip"
578+
, "JUMPI"
579+
, "raiseSlot_out:"
580+
, "JUMP"
581+
, "raiseSlot_skip:"
582+
, "PUSH2 raiseSlot_out"
583+
, "JUMP"
584+
, "raiseSlot_then:"
585+
, "SSTORE"
586+
, "DUP1"
587+
, "PUSH0"
588+
, "PUSH2 raiseSlot_check"
589+
, "JUMP"
547590
, "fn_setNonZero:"
548591
, "SWAP1"
549592
, "DUP1"

research/lean_only_proto/DumbContracts/Examples.lean

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

research/lean_only_proto/DumbContracts/Examples/UpdateMax.lean

Lines changed: 5 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -14,25 +14,23 @@ open DumbContracts.Std
1414
def updateMaxFun : Fun :=
1515
{ name := "updateMax"
1616
args := ["slot", "value"]
17-
body :=
18-
letSload "current" (v "slot")
19-
(sstoreMax (v "slot") (v "current") (v "value"))
17+
body := sstoreIfGt (v "slot") (v "value")
2018
ret := none }
2119

2220
def updateMaxSpec (slot value : Nat) : Spec Store :=
2321
{ requires := fun _ => True
24-
ensures := fun s s' => s' = updateStore s slot (if s slot > value then s slot else value) }
22+
ensures := fun s s' => s' = updateStore s slot (if value > s slot then value else s slot) }
2523

2624
theorem updateMax_meets_spec (s : Store) (slot value : Nat) :
2725
(updateMaxSpec slot value).requires s ->
2826
(match execFun updateMaxFun [slot, value] s [] with
2927
| ExecResult.ok _ s' => (updateMaxSpec slot value).ensures s s'
3028
| _ => False) := by
3129
intro _hreq
32-
by_cases h : s slot > value
33-
· simp [updateMaxFun, updateMaxSpec, letSload, sstoreMax, v, execFun, execStmt, evalExpr,
30+
by_cases h : value > s slot
31+
· simp [updateMaxFun, updateMaxSpec, sstoreIfGt, letSload, v, execFun, execStmt, evalExpr,
3432
bindArgs, emptyEnv, updateEnv, updateStore, h]
35-
· simp [updateMaxFun, updateMaxSpec, letSload, sstoreMax, v, execFun, execStmt, evalExpr,
33+
· simp [updateMaxFun, updateMaxSpec, sstoreIfGt, letSload, v, execFun, execStmt, evalExpr,
3634
bindArgs, emptyEnv, updateEnv, updateStore, h]
3735

3836
end DumbContracts.Examples

research/lean_only_proto/DumbContracts/Stdlib.lean

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -76,6 +76,13 @@ def sstoreIfLt (slot value : Expr) : Stmt :=
7676
(Stmt.sstore slot value)
7777
Stmt.skip)
7878

79+
def sstoreIfGt (slot value : Expr) : Stmt :=
80+
letSload "current" slot
81+
(Stmt.if_
82+
(Expr.gt value (Expr.var "current"))
83+
(Stmt.sstore slot value)
84+
Stmt.skip)
85+
7986
def sstoreAdd (slot delta : Expr) : Stmt :=
8087
Stmt.sstore slot (Expr.add (Expr.sload slot) delta)
8188

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 GeneratedRaiseSlotTest is GeneratedBase {
7+
function testRaiseSlot() public {
8+
bytes memory creation = _readHexFile("out/example.creation.bin");
9+
address deployed = _deploy(creation);
10+
11+
bytes4 selSetSlot = 0xf2c298be;
12+
bytes4 selRaiseSlot = 0x84c4b3e1;
13+
bytes4 selGet = 0x7eba7ba6;
14+
15+
(bool ok,) = deployed.call(abi.encodeWithSelector(selSetSlot, 5, 10));
16+
require(ok, "setSlot failed");
17+
18+
(ok,) = deployed.call(abi.encodeWithSelector(selRaiseSlot, 5, 20));
19+
require(ok, "raiseSlot failed (floor 20)");
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 == 20, "raiseSlot did not raise value");
26+
27+
(ok,) = deployed.call(abi.encodeWithSelector(selRaiseSlot, 5, 5));
28+
require(ok, "raiseSlot failed (floor 5)");
29+
(ok, data) = deployed.call(abi.encodeWithSelector(selGet, 5));
30+
require(ok, "getSlot failed (slot 5, after floor 5)");
31+
val = abi.decode(data, (uint256));
32+
require(val == 20, "raiseSlot should not lower value");
33+
}
34+
}

0 commit comments

Comments
 (0)