tempo_precompiles/storage/types/
slot.rs

1use alloy::primitives::U256;
2use std::marker::PhantomData;
3
4use crate::{
5    error::Result,
6    storage::{FieldLocation, Storable, StorageOps},
7};
8
9/// Type-safe wrapper for a single EVM storage slot.
10///
11/// # Type Parameters
12///
13/// - `T`: The Rust type stored in this slot (must implement `Storable<N>`)
14///
15/// # Example
16///
17/// ```ignore
18/// // Generated by #[contract] macro:
19/// pub mod slots {
20///     pub const NAME: U256 = uint!(2_U256);
21/// }
22/// let name_slot = Slot::<String>::new(slots::NAME);
23/// ```
24///
25/// The actual storage operations are handled by generated accessor methods
26/// that read/write values using the `PrecompileStorageProvider` trait.
27#[derive(Debug, Clone, Copy)]
28pub struct Slot<T> {
29    slot: U256,
30    ctx: crate::storage::types::LayoutCtx,
31    _ty: PhantomData<T>,
32}
33
34impl<T> Default for Slot<T> {
35    fn default() -> Self {
36        Self::new(U256::ZERO)
37    }
38}
39
40impl<T> Slot<T> {
41    /// Creates a new `Slot` with the given slot number.
42    ///
43    /// This is typically called with slot constants generated by the `#[contract]` macro.
44    /// Creates a full-slot accessor. For packed fields, use `new_at_loc` instead.
45    #[inline]
46    pub const fn new(slot: U256) -> Self {
47        Self {
48            slot,
49            ctx: crate::storage::types::LayoutCtx::FULL,
50            _ty: PhantomData,
51        }
52    }
53
54    /// Creates a new `Slot` with the given base slot number with the given offset.
55    ///
56    /// This is a convenience method for accessing struct fields.
57    /// Creates a full-slot accessor. For packed fields, use `new_at_loc` instead.
58    #[inline]
59    pub const fn new_at_offset(base_slot: U256, offset_slots: usize) -> Self {
60        Self {
61            slot: base_slot.saturating_add(U256::from_limbs([offset_slots as u64, 0, 0, 0])),
62            ctx: crate::storage::types::LayoutCtx::FULL,
63            _ty: PhantomData,
64        }
65    }
66
67    /// Creates a new `Slot` from a `FieldLocation` generated by the `#[derive(Storable)]` macro.
68    ///
69    /// This is the recommended way to access packed struct fields, combining slot offset
70    /// and byte offset information in a type-safe manner.
71    ///
72    /// This method should only be used with packable types (size < 32 bytes).
73    #[inline]
74    pub const fn new_at_loc<const N: usize>(base_slot: U256, loc: FieldLocation) -> Self
75    where
76        T: Storable<N>,
77    {
78        debug_assert!(
79            T::IS_PACKABLE,
80            "`fn new_at_loc` can only be used with packable types"
81        );
82        Self {
83            slot: base_slot.saturating_add(U256::from_limbs([loc.offset_slots as u64, 0, 0, 0])),
84            ctx: crate::storage::types::LayoutCtx::packed(loc.offset_bytes),
85            _ty: PhantomData,
86        }
87    }
88
89    /// Returns the U256 storage slot number.
90    #[inline]
91    pub const fn slot(&self) -> U256 {
92        self.slot
93    }
94
95    /// Returns the byte offset within the slot (for packed fields).
96    ///
97    /// Returns `Some(offset)` if this is a packed slot, `None` if it's a full slot.
98    #[inline]
99    pub const fn offset(&self) -> Option<usize> {
100        self.ctx.packed_offset()
101    }
102
103    /// Reads a value from storage at this slot.
104    ///
105    /// This method delegates to the `Storable::load` implementation,
106    /// which may read one or more consecutive slots depending on `N`.
107    ///
108    /// # Example
109    ///
110    /// ```ignore
111    /// let name_slot = Slot::<String>::new(slots::NAME);
112    /// let name = name_slot.read(&mut contract)?;
113    /// ```
114    #[inline]
115    pub fn read<S: StorageOps, const N: usize>(&self, storage: &mut S) -> Result<T>
116    where
117        T: Storable<N>,
118    {
119        T::load(storage, self.slot, self.ctx)
120    }
121
122    /// Writes a value to storage at this slot.
123    ///
124    /// This method delegates to the `Storable::store` implementation,
125    /// which may write one or more consecutive slots depending on `N`.
126    ///
127    /// # Example
128    ///
129    /// ```ignore
130    /// let name_slot = Slot::<String>::new(slots::NAME);
131    /// name_slot.write(&mut contract, "MyToken".to_string())?;
132    /// ```
133    #[inline]
134    pub fn write<S: StorageOps, const N: usize>(&self, storage: &mut S, value: T) -> Result<()>
135    where
136        T: Storable<N>,
137    {
138        value.store(storage, self.slot, self.ctx)
139    }
140
141    /// Deletes the value at this slot (sets all slots to zero).
142    ///
143    /// This method delegates to the `Storable::delete` implementation,
144    /// which sets `N` consecutive slots to zero.
145    ///
146    /// # Example
147    ///
148    /// ```ignore
149    /// let name_slot = Slot::<String>::new(slots::NAME);
150    /// name_slot.delete(&mut contract)?;
151    /// ```
152    #[inline]
153    pub fn delete<S: StorageOps, const N: usize>(&self, storage: &mut S) -> Result<()>
154    where
155        T: Storable<N>,
156    {
157        T::delete(storage, self.slot, self.ctx)
158    }
159}
160
161#[cfg(test)]
162mod tests {
163    use super::*;
164    use crate::storage::{
165        PrecompileStorageProvider, hashmap::HashMapStorageProvider, mapping_slot,
166    };
167    use alloy::primitives::{Address, B256};
168    use proptest::prelude::*;
169
170    // Test helper that implements StorageOps
171    struct TestContract<'a, S> {
172        address: Address,
173        storage: &'a mut S,
174    }
175
176    impl<'a, S: PrecompileStorageProvider> StorageOps for TestContract<'a, S> {
177        fn sstore(&mut self, slot: U256, value: U256) -> Result<()> {
178            self.storage.sstore(self.address, slot, value)
179        }
180
181        fn sload(&mut self, slot: U256) -> Result<U256> {
182            self.storage.sload(self.address, slot)
183        }
184    }
185
186    /// Helper to create a test contract with fresh storage.
187    fn setup_test_contract<'a>(
188        storage: &'a mut HashMapStorageProvider,
189    ) -> TestContract<'a, HashMapStorageProvider> {
190        TestContract {
191            address: Address::random(),
192            storage,
193        }
194    }
195
196    // Test slot constants
197    const TEST_SLOT_0: U256 = U256::ZERO;
198    const TEST_SLOT_1: U256 = U256::from_limbs([1, 0, 0, 0]);
199    const TEST_SLOT_2: U256 = U256::from_limbs([2, 0, 0, 0]);
200    const TEST_SLOT_MAX: U256 = U256::MAX;
201
202    // Property test strategies
203    fn arb_address() -> impl Strategy<Value = Address> {
204        any::<[u8; 20]>().prop_map(Address::from)
205    }
206
207    fn arb_u256() -> impl Strategy<Value = U256> {
208        any::<[u64; 4]>().prop_map(U256::from_limbs)
209    }
210
211    #[test]
212    fn test_slot_size() {
213        // slot (U256) 32 bytes + LayoutCtx (usize) 8 bytes + PhantomData (zero-sized)
214        assert_eq!(std::mem::size_of::<Slot<U256>>(), 40);
215        assert_eq!(std::mem::size_of::<Slot<Address>>(), 40);
216        assert_eq!(std::mem::size_of::<Slot<bool>>(), 40);
217    }
218
219    #[test]
220    fn test_slot_number_extraction() {
221        let slot_0 = Slot::<U256>::new(TEST_SLOT_0);
222        let slot_1 = Slot::<Address>::new(TEST_SLOT_1);
223        let slot_max = Slot::<bool>::new(TEST_SLOT_MAX);
224        assert_eq!(slot_0.slot(), U256::ZERO);
225        assert_eq!(slot_1.slot(), TEST_SLOT_1);
226        assert_eq!(slot_max.slot(), U256::MAX);
227    }
228
229    #[test]
230    fn test_slot_edge_case_zero() {
231        // Explicit test for U256::ZERO slot
232        let slot = Slot::<U256>::new(TEST_SLOT_0);
233        assert_eq!(slot.slot(), U256::ZERO);
234
235        let mut storage = HashMapStorageProvider::new(1);
236        let mut contract = setup_test_contract(&mut storage);
237        let value = U256::random();
238
239        _ = slot.write(&mut contract, value);
240        let loaded = slot.read(&mut contract).unwrap();
241        assert_eq!(loaded, value);
242    }
243
244    #[test]
245    fn test_slot_edge_case_max() {
246        // Explicit test for U256::MAX slot
247        let slot = Slot::<U256>::new(TEST_SLOT_MAX);
248        assert_eq!(slot.slot(), U256::MAX);
249
250        let mut storage = HashMapStorageProvider::new(1);
251        let mut contract = setup_test_contract(&mut storage);
252
253        let value = U256::random();
254        _ = slot.write(&mut contract, value);
255        let loaded = slot.read(&mut contract).unwrap();
256        assert_eq!(loaded, value);
257    }
258
259    #[test]
260    fn test_slot_read_write_u256() {
261        let mut storage = HashMapStorageProvider::new(1);
262        let mut contract = setup_test_contract(&mut storage);
263        let slot = Slot::<U256>::new(TEST_SLOT_1);
264        let test_value = U256::random();
265
266        // Write using new API
267        _ = slot.write(&mut contract, test_value);
268
269        // Read using new API
270        let loaded = slot.read(&mut contract).unwrap();
271        assert_eq!(loaded, test_value);
272
273        // Verify it actually wrote to slot 1
274        let raw = contract.storage.sload(contract.address, TEST_SLOT_1);
275        assert_eq!(raw, Ok(test_value));
276    }
277
278    #[test]
279    fn test_slot_read_write_address() {
280        let mut storage = HashMapStorageProvider::new(1);
281        let mut contract = setup_test_contract(&mut storage);
282        let test_addr = Address::random();
283        let slot = Slot::<Address>::new(TEST_SLOT_1);
284
285        // Write
286        _ = slot.write(&mut contract, test_addr);
287
288        // Read
289        let loaded = slot.read(&mut contract).unwrap();
290        assert_eq!(loaded, test_addr);
291    }
292
293    #[test]
294    fn test_slot_read_write_bool() {
295        let mut storage = HashMapStorageProvider::new(1);
296        let mut contract = setup_test_contract(&mut storage);
297        let slot = Slot::<bool>::new(TEST_SLOT_1);
298
299        // Write true
300        _ = slot.write(&mut contract, true);
301        assert!(slot.read(&mut contract).unwrap());
302
303        // Write false
304        _ = slot.write(&mut contract, false);
305        assert!(!slot.read(&mut contract).unwrap());
306    }
307
308    #[test]
309    fn test_slot_read_write_string() {
310        let mut storage = HashMapStorageProvider::new(1);
311        let mut contract = setup_test_contract(&mut storage);
312        let slot = Slot::<String>::new(TEST_SLOT_1);
313
314        let test_name = "TestToken";
315        _ = slot.write(&mut contract, test_name.to_string());
316
317        let loaded = slot.read(&mut contract).unwrap();
318        assert_eq!(loaded, test_name);
319    }
320
321    #[test]
322    fn test_slot_default_value_is_zero() {
323        let mut storage = HashMapStorageProvider::new(1);
324        let mut contract = setup_test_contract(&mut storage);
325        let slot = Slot::<U256>::new(TEST_SLOT_1);
326
327        // Reading uninitialized storage should return zero
328        let value = slot.read(&mut contract).unwrap();
329        assert_eq!(value, U256::ZERO);
330    }
331
332    #[test]
333    fn test_slot_overwrite() {
334        let mut storage = HashMapStorageProvider::new(1);
335        let mut contract = setup_test_contract(&mut storage);
336        let slot = Slot::<u64>::new(TEST_SLOT_1);
337
338        // Write initial value
339        _ = slot.write(&mut contract, 100);
340        assert_eq!(slot.read(&mut contract), Ok(100));
341
342        // Overwrite with new value
343        _ = slot.write(&mut contract, 200);
344        assert_eq!(slot.read(&mut contract), Ok(200));
345    }
346
347    proptest! {
348        #![proptest_config(ProptestConfig::with_cases(500))]
349
350        #[test]
351        fn proptest_slot_read_write_u256(value in arb_u256()) {
352            let mut storage = HashMapStorageProvider::new(1);
353            let mut contract = setup_test_contract(&mut storage);
354            let slot = Slot::<U256>::new(TEST_SLOT_2);
355
356            // Write and read back
357            slot.write(&mut contract, value)?;
358            let loaded = slot.read(&mut contract)?;
359            prop_assert_eq!(loaded, value, "roundtrip failed");
360
361            // Delete and verify
362            slot.delete(&mut contract)?;
363            let after_delete = slot.read(&mut contract)?;
364            prop_assert_eq!(after_delete, U256::ZERO, "not zero after delete");
365        }
366
367        #[test]
368        fn proptest_slot_read_write_address(addr_value in arb_address()) {
369            let mut storage = HashMapStorageProvider::new(1);
370            let mut contract = setup_test_contract(&mut storage);
371            let slot = Slot::<Address>::new(TEST_SLOT_1);
372
373            // Write and read back
374            slot.write(&mut contract, addr_value)?;
375            let loaded = slot.read(&mut contract)?;
376            prop_assert_eq!(loaded, addr_value, "address roundtrip failed");
377        }
378
379        #[test]
380        fn proptest_slot_isolation(value1 in arb_u256(), value2 in arb_u256()) {
381            let mut storage = HashMapStorageProvider::new(1);
382            let mut contract = setup_test_contract(&mut storage);
383            let slot1 = Slot::<U256>::new(TEST_SLOT_1);
384            let slot2 = Slot::<U256>::new(TEST_SLOT_2);
385
386            slot1.write(&mut contract, value1)?;
387            slot2.write(&mut contract, value2)?;
388
389            // Verify both slots retain their independent values
390            let loaded1 = slot1.read(&mut contract)?;
391            let loaded2 = slot2.read(&mut contract)?;
392
393            prop_assert_eq!(loaded1, value1, "slot 1 value changed");
394            prop_assert_eq!(loaded2, value2, "slot 2 value changed");
395
396            // Delete slot 1, verify slot 2 unaffected
397            slot1.delete(&mut contract)?;
398            let after_delete1 = slot1.read(&mut contract)?;
399            let after_delete2 = slot2.read(&mut contract)?;
400
401            prop_assert_eq!(after_delete1, U256::ZERO, "slot 1 not deleted");
402            prop_assert_eq!(after_delete2, value2, "slot 2 affected by slot 1 delete");
403        }
404    }
405
406    // -- RUNTIME SLOT OFFSET TESTS --------------------------------------------
407
408    #[test]
409    fn test_slot_at_offset() -> eyre::Result<()> {
410        let mut storage = HashMapStorageProvider::new(1);
411        let mut contract = setup_test_contract(&mut storage);
412
413        let pair_key: B256 = U256::from(0xabcd).into();
414        let orderbook_base_slot = mapping_slot(pair_key, U256::ZERO);
415
416        let base_address = Address::random();
417
418        // Write to orderbook.base using runtime offset
419        let slot = Slot::<Address>::new_at_offset(orderbook_base_slot, 0);
420        slot.write(&mut contract, base_address)?;
421
422        // Read back
423        let read_address = slot.read(&mut contract)?;
424
425        assert_eq!(read_address, base_address);
426
427        // Delete
428        slot.delete(&mut contract)?;
429
430        let deleted = slot.read(&mut contract)?;
431
432        assert_eq!(deleted, Address::ZERO);
433
434        Ok(())
435    }
436
437    #[test]
438    fn test_slot_at_offset_multi_slot_value() -> eyre::Result<()> {
439        let mut storage = HashMapStorageProvider::new(1);
440        let mut contract = setup_test_contract(&mut storage);
441
442        let struct_key: B256 = U256::from(0xabcd).into();
443        let struct_base = mapping_slot(struct_key, U256::ZERO);
444
445        // Test writing a multi-slot value like a String at offset 2
446        let test_string = "Hello, Orderbook!".to_string();
447
448        let slot = Slot::<String>::new_at_offset(struct_base, 2);
449        slot.write(&mut contract, test_string.clone())?;
450
451        let read_string = slot.read(&mut contract)?;
452
453        assert_eq!(read_string, test_string);
454
455        Ok(())
456    }
457
458    #[test]
459    fn test_multiple_primitive_fields() -> eyre::Result<()> {
460        let mut storage = HashMapStorageProvider::new(1);
461        let mut contract = setup_test_contract(&mut storage);
462
463        let key: B256 = U256::from(0x9999).into();
464        let base = mapping_slot(key, U256::ZERO);
465
466        // Simulate different primitive fields at different offsets
467        let field_0 = Address::random();
468        let field_1: u64 = 12345;
469        let field_2 = U256::from(999999);
470
471        Slot::<Address>::new_at_offset(base, 0).write(&mut contract, field_0)?;
472        Slot::<u64>::new_at_offset(base, 1).write(&mut contract, field_1)?;
473        Slot::<U256>::new_at_offset(base, 2).write(&mut contract, field_2)?;
474
475        // Verify independence
476        let read_0 = Slot::<Address>::new_at_offset(base, 0).read(&mut contract)?;
477        let read_1 = Slot::<u64>::new_at_offset(base, 1).read(&mut contract)?;
478        let read_2 = Slot::<U256>::new_at_offset(base, 2).read(&mut contract)?;
479
480        assert_eq!(read_0, field_0);
481        assert_eq!(read_1, field_1);
482        assert_eq!(read_2, field_2);
483
484        Ok(())
485    }
486}