tempo_precompiles/storage/types/
mapping.rs

1//! Type-safe wrapper for EVM storage mappings (hash-based key-value storage).
2
3use alloy::primitives::{U256, keccak256};
4use std::marker::PhantomData;
5
6use crate::storage::{Layout, Slot, Storable, StorableType, StorageKey};
7
8/// Type-safe access wrapper for EVM storage mappings (hash-based key-value storage).
9///
10/// This struct does not store data itself. Instead, it provides a zero-cost abstraction
11/// for accessing mapping storage slots using Solidity's hash-based layout. It wraps a
12/// base slot number and provides methods to compute the actual storage slots for keys.
13///
14/// # Type Parameters
15///
16/// - `K`: Key type (must implement `StorageKey`)
17/// - `V`: Value type (must implement `Storable<N>`)
18///
19/// # Storage Layout
20///
21/// Mappings use Solidity's storage layout:
22/// - Base slot: stored in `base_slot` field (never accessed directly)
23/// - Actual slot for key `k`: `keccak256(k || base_slot)`
24///
25/// # Usage Pattern
26///
27/// The typical usage follows a composable pattern:
28/// 1. Create a `Mapping<K, V>` with a base slot (usually from generated constants)
29/// 2. Call `.at(key)` to compute and obtain a `Slot<V>` for that key
30/// 3. Use `.read()`, `.write()`, or `.delete()` on the resulting slot
31///
32/// # Accessing Mapping Fields Within Structs
33///
34/// When a mapping is a field within a struct stored in another mapping, use the static
35/// `at_offset` method to compute the slot without creating a `Mapping` instance:
36///
37/// # Relationship with `Slot<V>`
38///
39/// `Mapping<K, V>` is essentially a slot computation helper. The `.at(key)` method
40/// performs the keccak256 hash to compute the actual storage slot and returns a
41/// `Slot<V>` that can be used for read/write operations.
42#[derive(Debug, Clone, Copy)]
43pub struct Mapping<K, V> {
44    base_slot: U256,
45    _phantom: PhantomData<(K, V)>,
46}
47
48impl<K, V> Mapping<K, V> {
49    /// Creates a new `Mapping` with the given base slot number.
50    ///
51    /// This is typically called with slot constants generated by the `#[contract]` macro.
52    #[inline]
53    pub const fn new(base_slot: U256) -> Self {
54        Self {
55            base_slot,
56            _phantom: PhantomData,
57        }
58    }
59
60    /// Returns the U256 base storage slot number for this mapping.
61    #[inline]
62    pub const fn slot(&self) -> U256 {
63        self.base_slot
64    }
65
66    /// Returns a `Slot<V>` for the given key.
67    ///
68    /// This enables the composable pattern: `mapping.at(key).read(storage)`
69    /// where the mapping slot computation happens once, and the resulting slot
70    /// can be used for multiple operations.
71    pub fn at<const N: usize>(&self, key: K) -> Slot<V>
72    where
73        K: StorageKey,
74        V: Storable<N>,
75    {
76        Slot::new(mapping_slot(key.as_storage_bytes(), self.base_slot))
77    }
78
79    /// Returns a `Slot<V>` for a mapping field within a struct at a given base slot.
80    ///
81    /// This method enables accessing mapping fields within structs when you have
82    /// the struct's base slot at runtime and know the field's offset.
83    #[inline]
84    pub fn at_offset<const N: usize>(
85        struct_base_slot: U256,
86        field_offset_slots: usize,
87        key: K,
88    ) -> Slot<V>
89    where
90        K: StorageKey,
91        V: Storable<N>,
92    {
93        let field_slot = struct_base_slot + U256::from(field_offset_slots);
94        Slot::new(mapping_slot(key.as_storage_bytes(), field_slot))
95    }
96}
97
98/// Type-safe access wrapper for nested EVM storage mappings (two-level hash-based storage).
99///
100/// Like `Mapping<K, V>`, this struct does not store data. It provides a zero-cost abstraction
101/// for accessing nested mapping storage using Solidity's double-hash layout. It wraps a base
102/// slot and provides methods to navigate through two levels of key lookups.
103///
104/// # Type Parameters
105///
106/// - `K1`: First-level key type (must implement `StorageKey`)
107/// - `K2`: Second-level key type (must implement `StorageKey`)
108/// - `V`: Value type (must implement `Storable<N>`)
109///
110/// # Storage Layout
111///
112/// Nested mappings use a two-step hashing process:
113/// - Base slot: stored in the inner mapping's `base_slot`
114/// - Intermediate slot for `k1`: `keccak256(k1 || base_slot)`
115/// - Final slot for `k1, k2`: `keccak256(k2 || intermediate_slot)`
116///
117/// # Usage Pattern
118///
119/// The typical usage follows a two-step composable pattern:
120/// 1. Create a `NestedMapping<K1, K2, V>` with a base slot
121/// 2. Call `.at(key1)` to get an intermediate `Mapping<K2, V>`
122/// 3. Call `.at(key2)` on the intermediate mapping to get a `Slot<V>`
123/// 4. Use `.read()`, `.write()`, or `.delete()` on the resulting slot
124///
125/// # Accessing Nested Mapping Fields Within Structs
126///
127/// When a nested mapping is a field within a struct, you can manually compute the field's
128/// slot by adding the offset to the struct's base slot, then create a new `NestedMapping`:
129///
130/// # Relationship with `Mapping<K, V>` and `Slot<V>`
131///
132/// `NestedMapping<K1, K2, V>` internally wraps `Mapping<K1, Mapping<K2, V>>`. The first
133/// `.at(k1)` call performs the first hash to compute an intermediate slot and returns a
134/// `Mapping<K2, V>`. The second `.at(k2)` call on that mapping performs the second hash
135/// and returns a `Slot<V>` for read/write operations.
136#[derive(Debug, Clone, Copy)]
137pub struct NestedMapping<K1, K2, V> {
138    _inner: Mapping<K1, Mapping<K2, V>>,
139}
140
141impl<K1, K2, V> NestedMapping<K1, K2, V> {
142    /// Creates a new `NestedMapping` with the given base slot number.
143    ///
144    /// This is typically called with slot constants generated by the `#[contract]` macro.
145    #[inline]
146    pub const fn new(base_slot: U256) -> Self {
147        Self {
148            _inner: Mapping::new(base_slot),
149        }
150    }
151
152    /// Returns the U256 base storage slot number for this nested mapping.
153    #[inline]
154    pub const fn slot(&self) -> U256 {
155        self._inner.slot()
156    }
157
158    /// Returns a `Mapping<K2, V>` for the given first-level key.
159    #[inline]
160    pub fn at(&self, key1: K1) -> Mapping<K2, V>
161    where
162        K1: StorageKey,
163    {
164        let intermediate_slot = mapping_slot(key1.as_storage_bytes(), self._inner.base_slot);
165        Mapping::new(intermediate_slot)
166    }
167}
168
169impl<K, V> Default for Mapping<K, V> {
170    fn default() -> Self {
171        Self::new(U256::ZERO)
172    }
173}
174
175impl<K1, K2, V> Default for NestedMapping<K1, K2, V> {
176    fn default() -> Self {
177        Self::new(U256::ZERO)
178    }
179}
180
181// Mappings occupy a full 32-byte slot in the layout (used as a base for hashing),
182// even though they don't store data in that slot directly.
183//
184// **NOTE:** Necessary to allow it to participate in struct layout calculations.
185impl<K, V> StorableType for Mapping<K, V> {
186    const LAYOUT: Layout = Layout::Slots(1);
187}
188
189// Nested mappings occupy a full 32-byte slot in the layout (used as a base for hashing),
190// even though they don't store data in that slot directly.
191//
192// **NOTE:** Necessary to allow it to participate in struct layout calculations.
193impl<K1, K2, V> StorableType for NestedMapping<K1, K2, V> {
194    const LAYOUT: Layout = Layout::Slots(1);
195}
196
197// -- HELPER FUNCTIONS ---------------------------------------------------------
198
199fn left_pad_to_32(data: &[u8]) -> [u8; 32] {
200    let mut buf = [0u8; 32];
201    buf[32 - data.len()..].copy_from_slice(data);
202    buf
203}
204
205/// Compute storage slot for a mapping
206#[inline]
207pub fn mapping_slot<T: AsRef<[u8]>>(key: T, mapping_slot: U256) -> U256 {
208    let mut buf = [0u8; 64];
209    buf[..32].copy_from_slice(&left_pad_to_32(key.as_ref()));
210    buf[32..].copy_from_slice(&mapping_slot.to_be_bytes::<32>());
211    U256::from_be_bytes(keccak256(buf).0)
212}
213
214/// Compute storage slot for a double mapping (mapping\[key1\]\[key2\])
215#[inline]
216pub fn double_mapping_slot<T: AsRef<[u8]>, U: AsRef<[u8]>>(
217    key1: T,
218    key2: U,
219    base_slot: U256,
220) -> U256 {
221    let intermediate_slot = mapping_slot(key1, base_slot);
222    let mut buf = [0u8; 64];
223    buf[..32].copy_from_slice(&left_pad_to_32(key2.as_ref()));
224    buf[32..].copy_from_slice(&intermediate_slot.to_be_bytes::<32>());
225    U256::from_be_bytes(keccak256(buf).0)
226}
227
228#[cfg(test)]
229mod tests {
230    use super::*;
231    use crate::{
232        error::Result,
233        storage::{PrecompileStorageProvider, StorageOps, hashmap::HashMapStorageProvider},
234    };
235    use alloy::primitives::{Address, B256, address};
236    use proptest::prelude::*;
237
238    // Test helper that implements StorageOps
239    struct TestContract<'a, S> {
240        address: Address,
241        storage: &'a mut S,
242    }
243
244    impl<'a, S: PrecompileStorageProvider> StorageOps for TestContract<'a, S> {
245        fn sstore(&mut self, slot: U256, value: U256) -> Result<()> {
246            self.storage.sstore(self.address, slot, value)
247        }
248
249        fn sload(&mut self, slot: U256) -> Result<U256> {
250            self.storage.sload(self.address, slot)
251        }
252    }
253
254    /// Helper to create a test contract with fresh storage.
255    fn setup_test_contract<'a>(
256        storage: &'a mut HashMapStorageProvider,
257    ) -> TestContract<'a, HashMapStorageProvider> {
258        TestContract {
259            address: Address::random(),
260            storage,
261        }
262    }
263
264    // Test slot constants
265    const TEST_SLOT_0: U256 = U256::ZERO;
266    const TEST_SLOT_1: U256 = U256::from_limbs([1, 0, 0, 0]);
267    const TEST_SLOT_2: U256 = U256::from_limbs([2, 0, 0, 0]);
268    const TEST_SLOT_MAX: U256 = U256::MAX;
269
270    // Property test strategies
271    fn arb_address() -> impl Strategy<Value = Address> {
272        any::<[u8; 20]>().prop_map(Address::from)
273    }
274
275    fn arb_u256() -> impl Strategy<Value = U256> {
276        any::<[u64; 4]>().prop_map(U256::from_limbs)
277    }
278
279    #[test]
280    fn test_mapping_slot_deterministic() {
281        let key: B256 = U256::from(123).into();
282        let slot1 = mapping_slot(key, U256::ZERO);
283        let slot2 = mapping_slot(key, U256::ZERO);
284
285        assert_eq!(slot1, slot2);
286    }
287
288    #[test]
289    fn test_different_keys_different_slots() {
290        let key1: B256 = U256::from(123).into();
291        let key2: B256 = U256::from(456).into();
292
293        let slot1 = mapping_slot(key1, U256::ZERO);
294        let slot2 = mapping_slot(key2, U256::ZERO);
295
296        assert_ne!(slot1, slot2);
297    }
298
299    #[test]
300    fn test_tip20_balance_slots() {
301        // Test balance slot calculation for TIP20 tokens (slot 10)
302        let alice = address!("0xf39Fd6e51aad88F6F4ce6aB8827279cffFb92266");
303        let bob = address!("0x70997970C51812dc3A010C7d01b50e0d17dc79C8");
304
305        let alice_balance_slot = mapping_slot(alice, U256::from(10));
306        let bob_balance_slot = mapping_slot(bob, U256::from(10));
307
308        println!("Alice balance slot: 0x{alice_balance_slot:064x}");
309        println!("Bob balance slot: 0x{bob_balance_slot:064x}");
310
311        // Verify they're different
312        assert_ne!(alice_balance_slot, bob_balance_slot);
313    }
314
315    #[test]
316    fn test_tip20_allowance_slots() {
317        // Test allowance slot calculation for TIP20 tokens (slot 11)
318        let alice = address!("0xf39Fd6e51aad88F6F4ce6aB8827279cffFb92266");
319        let tip_fee_mgr = address!("0xfeec000000000000000000000000000000000000");
320
321        let allowances = NestedMapping::<Address, Address, U256>::new(U256::from(11));
322        let allowance_slot = allowances.at(alice).at(tip_fee_mgr).slot();
323
324        println!("Alice->TipFeeManager allowance slot: 0x{allowance_slot:064x}");
325
326        // Just verify it's calculated consistently
327        let allowance_slot2 = allowances.at(alice).at(tip_fee_mgr).slot();
328        assert_eq!(allowance_slot, allowance_slot2);
329    }
330
331    #[test]
332    fn test_double_mapping_different_keys() {
333        let alice = address!("0xf39Fd6e51aad88F6F4ce6aB8827279cffFb92266");
334        let bob = address!("0x70997970C51812dc3A010C7d01b50e0d17dc79C8");
335        let spender = address!("0xfeec000000000000000000000000000000000000");
336
337        let allowances = NestedMapping::<Address, Address, U256>::new(U256::from(11));
338        let alice_allowance = allowances.at(alice).at(spender).slot();
339        let bob_allowance = allowances.at(bob).at(spender).slot();
340
341        assert_ne!(alice_allowance, bob_allowance);
342    }
343
344    #[test]
345    fn test_left_padding_correctness() {
346        let addr = address!("0xf39Fd6e51aad88F6F4ce6aB8827279cffFb92266");
347        let bytes: &[u8] = addr.as_ref();
348        let padded = left_pad_to_32(bytes);
349
350        // First 12 bytes should be zeros (left padding)
351        assert_eq!(&padded[..12], &[0u8; 12]);
352        // Last 20 bytes should be the address
353        assert_eq!(&padded[12..], bytes);
354    }
355
356    #[test]
357    fn test_mapping_slot_encoding() {
358        let key = address!("0xf39Fd6e51aad88F6F4ce6aB8827279cffFb92266");
359        let base_slot = U256::from(10);
360
361        // Manual computation to validate
362        let mut buf = [0u8; 64];
363        // Left-pad the address to 32 bytes
364        buf[12..32].copy_from_slice(key.as_ref());
365        // Slot in big-endian
366        buf[32..].copy_from_slice(&base_slot.to_be_bytes::<32>());
367
368        let expected = U256::from_be_bytes(keccak256(buf).0);
369        let computed = mapping_slot(key, base_slot);
370
371        assert_eq!(computed, expected, "mapping_slot encoding mismatch");
372    }
373
374    #[test]
375    fn test_double_mapping_account_role() {
376        let account = address!("0xf39Fd6e51aad88F6F4ce6aB8827279cffFb92266");
377        let role: B256 = U256::ONE.into();
378        let base_slot = U256::ONE;
379
380        let roles = NestedMapping::<Address, B256, U256>::new(base_slot);
381        let slot = roles.at(account).at(role).slot();
382
383        // Verify deterministic
384        let slot2 = roles.at(account).at(role).slot();
385        assert_eq!(slot, slot2);
386
387        // Verify different role yields different slot
388        let different_role: B256 = U256::from(2).into();
389        let different_slot = roles.at(account).at(different_role).slot();
390        assert_ne!(slot, different_slot);
391    }
392
393    #[test]
394    fn test_mapping_size() {
395        // Mapping now contains a U256 base_slot field (32 bytes)
396        assert_eq!(std::mem::size_of::<Mapping<Address, U256>>(), 32);
397        assert_eq!(std::mem::size_of::<Mapping<U256, Address>>(), 32);
398        // Nested mapping also contains a U256 (outer mapping's slot)
399        assert_eq!(
400            std::mem::size_of::<Mapping<Address, Mapping<Address, U256>>>(),
401            32
402        );
403    }
404
405    #[test]
406    fn test_mapping_creation() {
407        let _simple: Mapping<Address, U256> = Mapping::new(U256::ZERO);
408        let _another: Mapping<U256, bool> = Mapping::new(U256::ONE);
409    }
410
411    #[test]
412    fn test_mapping_slot_extraction() {
413        assert_eq!(Mapping::<Address, U256>::new(U256::ONE).slot(), U256::ONE);
414        assert_eq!(
415            Mapping::<U256, Address>::new(U256::from(2)).slot(),
416            U256::from(2)
417        );
418
419        // Test with larger slot number
420        assert_eq!(Mapping::<Address, U256>::new(U256::MAX).slot(), U256::MAX);
421    }
422
423    #[test]
424    fn test_mapping_edge_case_zero() {
425        // Explicit test for U256::ZERO base slot
426        assert_eq!(Mapping::<Address, U256>::new(U256::ZERO).slot(), U256::ZERO);
427
428        let mut storage = HashMapStorageProvider::new(1);
429        let mut contract = setup_test_contract(&mut storage);
430        let user = Address::random();
431
432        let zero_mapping = Mapping::<Address, U256>::new(U256::ZERO);
433        let value = U256::from(1000u64);
434
435        _ = zero_mapping.at(user).write(&mut contract, value);
436        let loaded = zero_mapping.at(user).read(&mut contract).unwrap();
437        assert_eq!(loaded, value);
438    }
439
440    #[test]
441    fn test_mapping_edge_case_max() {
442        // Explicit test for U256::MAX base slot
443        let max_mapping = Mapping::<Address, U256>::new(U256::MAX);
444        assert_eq!(max_mapping.slot(), U256::MAX);
445
446        let mut storage = HashMapStorageProvider::new(1);
447        let mut contract = setup_test_contract(&mut storage);
448        let user = Address::random();
449
450        let value = U256::from(999u64);
451        _ = max_mapping.at(user).write(&mut contract, value);
452        let loaded = max_mapping.at(user).read(&mut contract).unwrap();
453        assert_eq!(loaded, value);
454    }
455
456    #[test]
457    fn test_mapping_read_write_balances() {
458        let mut storage = HashMapStorageProvider::new(1);
459        let mut contract = setup_test_contract(&mut storage);
460        let user1 = Address::random();
461        let user2 = Address::random();
462
463        let named_mapping = Mapping::<Address, U256>::new(TEST_SLOT_1);
464
465        let balance1 = U256::from(1000u64);
466        let balance2 = U256::from(2000u64);
467
468        // Write balances
469        _ = named_mapping.at(user1).write(&mut contract, balance1);
470        _ = named_mapping.at(user2).write(&mut contract, balance2);
471
472        // Read balances
473        let loaded1 = named_mapping.at(user1).read(&mut contract).unwrap();
474        let loaded2 = named_mapping.at(user2).read(&mut contract).unwrap();
475
476        assert_eq!(loaded1, balance1);
477        assert_eq!(loaded2, balance2);
478    }
479
480    #[test]
481    fn test_mapping_read_default_is_zero() {
482        let mut storage = HashMapStorageProvider::new(1);
483        let mut contract = setup_test_contract(&mut storage);
484        let user = Address::random();
485
486        let named_mapping = Mapping::<Address, U256>::new(TEST_SLOT_1);
487
488        // Reading uninitialized mapping slot should return zero
489        let balance = named_mapping.at(user).read(&mut contract).unwrap();
490        assert_eq!(balance, U256::ZERO);
491    }
492
493    #[test]
494    fn test_mapping_overwrite() {
495        let mut storage = HashMapStorageProvider::new(1);
496        let mut contract = setup_test_contract(&mut storage);
497        let user = Address::random();
498
499        let named_mapping = Mapping::<Address, U256>::new(TEST_SLOT_1);
500
501        // Write initial balance
502        _ = named_mapping.at(user).write(&mut contract, U256::from(100));
503        assert_eq!(
504            named_mapping.at(user).read(&mut contract),
505            Ok(U256::from(100))
506        );
507
508        // Overwrite with new balance
509        _ = named_mapping.at(user).write(&mut contract, U256::from(200));
510        assert_eq!(
511            named_mapping.at(user).read(&mut contract),
512            Ok(U256::from(200))
513        );
514    }
515
516    #[test]
517    fn test_nested_mapping_read_write_allowances() {
518        let mut storage = HashMapStorageProvider::new(1);
519        let mut contract = setup_test_contract(&mut storage);
520        let owner = Address::random();
521        let spender1 = Address::random();
522        let spender2 = Address::random();
523
524        // Nested mapping: outer slot is 11, inner slot is dummy (unused)
525        let nested_mapping = NestedMapping::<Address, Address, U256>::new(TEST_SLOT_1);
526
527        let allowance1 = U256::from(500u64);
528        let allowance2 = U256::from(1500u64);
529
530        // Write allowances using nested API
531        _ = nested_mapping
532            .at(owner)
533            .at(spender1)
534            .write(&mut contract, allowance1);
535        _ = nested_mapping
536            .at(owner)
537            .at(spender2)
538            .write(&mut contract, allowance2);
539
540        // Read allowances using nested API
541        let loaded1 = nested_mapping
542            .at(owner)
543            .at(spender1)
544            .read(&mut contract)
545            .unwrap();
546        let loaded2 = nested_mapping
547            .at(owner)
548            .at(spender2)
549            .read(&mut contract)
550            .unwrap();
551
552        assert_eq!(loaded1, allowance1);
553        assert_eq!(loaded2, allowance2);
554    }
555
556    #[test]
557    fn test_nested_mapping_default_is_zero() {
558        let mut storage = HashMapStorageProvider::new(1);
559        let mut contract = setup_test_contract(&mut storage);
560        let owner = Address::random();
561        let spender = Address::random();
562
563        let nested_mapping = NestedMapping::<Address, Address, U256>::new(TEST_SLOT_1);
564
565        // Reading uninitialized nested mapping should return zero
566        let allowance = nested_mapping
567            .at(owner)
568            .at(spender)
569            .read(&mut contract)
570            .unwrap();
571        assert_eq!(allowance, U256::ZERO);
572    }
573
574    #[test]
575    fn test_nested_mapping_independence() {
576        let mut storage = HashMapStorageProvider::new(1);
577        let mut contract = setup_test_contract(&mut storage);
578        let owner1 = Address::random();
579        let owner2 = Address::random();
580        let spender = Address::random();
581
582        let nested_mapping = NestedMapping::<Address, Address, U256>::new(TEST_SLOT_1);
583
584        // Set allowance for owner1 -> spender
585        _ = nested_mapping
586            .at(owner1)
587            .at(spender)
588            .write(&mut contract, U256::from(100));
589
590        // Verify owner2 -> spender is still zero (independent slot)
591        let allowance2 = nested_mapping
592            .at(owner2)
593            .at(spender)
594            .read(&mut contract)
595            .unwrap();
596        assert_eq!(allowance2, U256::ZERO);
597
598        // Verify owner1 -> spender is unchanged
599        let allowance1 = nested_mapping
600            .at(owner1)
601            .at(spender)
602            .read(&mut contract)
603            .unwrap();
604        assert_eq!(allowance1, U256::from(100));
605    }
606
607    #[test]
608    fn test_mapping_with_different_key_types() {
609        let mut storage = HashMapStorageProvider::new(1);
610        let mut contract = setup_test_contract(&mut storage);
611
612        // Mapping with U256 key
613        let nonces_mapping = Mapping::<Address, U256>::new(TEST_SLOT_2);
614        let user = Address::random();
615        let nonce = U256::from(42);
616
617        _ = nonces_mapping.at(user).write(&mut contract, nonce);
618        let loaded_nonce = nonces_mapping.at(user).read(&mut contract).unwrap();
619        assert_eq!(loaded_nonce, nonce);
620
621        // Mapping with bool value
622        let flags_mapping = Mapping::<Address, bool>::new(TEST_SLOT_MAX);
623        _ = flags_mapping.at(user).write(&mut contract, true);
624        let loaded_flag = flags_mapping.at(user).read(&mut contract).unwrap();
625        assert!(loaded_flag);
626    }
627
628    proptest! {
629        #![proptest_config(ProptestConfig::with_cases(500))]
630
631        #[test]
632        fn proptest_mapping_read_write(
633            key in arb_address(),
634            value in arb_u256()
635        ) {
636            let mut storage = HashMapStorageProvider::new(1);
637            let mut contract = setup_test_contract(&mut storage);
638
639            // Test with TestSlot10
640        let test_mapping = Mapping::<Address, U256>::new(TEST_SLOT_0);
641
642            // Write and read back
643            test_mapping.at(key).write(&mut contract, value)?;
644            let loaded = test_mapping.at(key).read(&mut contract)?;
645            prop_assert_eq!(loaded, value, "roundtrip failed");
646
647            // Delete and verify
648            test_mapping.at(key).delete(&mut contract)?;
649            let after_delete = test_mapping.at(key).read(&mut contract)?;
650            prop_assert_eq!(after_delete, U256::ZERO, "not zero after delete");
651        }
652
653        #[test]
654        fn proptest_mapping_key_isolation(
655            key1 in arb_address(),
656            key2 in arb_address(),
657            value1 in arb_u256(),
658            value2 in arb_u256()
659        ) {
660            // Skip if keys are the same
661            prop_assume!(key1 != key2);
662
663            let mut storage = HashMapStorageProvider::new(1);
664            let mut contract = setup_test_contract(&mut storage);
665
666        let test_mapping = Mapping::<Address, U256>::new(TEST_SLOT_0);
667
668            // Write different values to different keys
669            test_mapping.at(key1).write(&mut contract, value1)?;
670            test_mapping.at(key2).write(&mut contract, value2)?;
671
672            // Verify both keys retain their independent values
673            let loaded1 = test_mapping.at(key1).read(&mut contract)?;
674            let loaded2 = test_mapping.at(key2).read(&mut contract)?;
675
676            prop_assert_eq!(loaded1, value1, "key1 value changed");
677            prop_assert_eq!(loaded2, value2, "key2 value changed");
678
679            // Delete key1, verify key2 unaffected
680            test_mapping.at(key1).delete(&mut contract)?;
681            let after_delete1 = test_mapping.at(key1).read(&mut contract)?;
682            let after_delete2 = test_mapping.at(key2).read(&mut contract)?;
683
684            prop_assert_eq!(after_delete1, U256::ZERO, "key1 not deleted");
685            prop_assert_eq!(after_delete2, value2, "key2 affected by key1 delete");
686        }
687
688        #[test]
689        fn proptest_nested_mapping_isolation(
690            owner1 in arb_address(),
691            owner2 in arb_address(),
692            spender in arb_address(),
693            allowance1 in arb_u256(),
694            allowance2 in arb_u256()
695        ) {
696            // Skip if owners are the same
697            prop_assume!(owner1 != owner2);
698
699            let mut storage = HashMapStorageProvider::new(1);
700            let mut contract = setup_test_contract(&mut storage);
701
702        let nested_mapping = NestedMapping::<Address, Address, U256>::new(TEST_SLOT_1);
703
704            // Write different allowances for different owners
705            nested_mapping.at(owner1).at(spender).write(&mut contract, allowance1)?;
706            nested_mapping.at(owner2).at(spender).write(&mut contract, allowance2)?;
707
708            // Verify both owners' allowances are independent
709            let loaded1 = nested_mapping.at(owner1).at(spender).read(&mut contract)?;
710            let loaded2 = nested_mapping.at(owner2).at(spender).read(&mut contract)?;
711
712            prop_assert_eq!(loaded1, allowance1, "owner1 allowance changed");
713            prop_assert_eq!(loaded2, allowance2, "owner2 allowance changed");
714
715            // Delete owner1's allowance, verify owner2 unaffected
716            nested_mapping.at(owner1).at(spender).delete(&mut contract)?;
717            let after_delete1 = nested_mapping.at(owner1).at(spender).read(&mut contract)?;
718            let after_delete2 = nested_mapping.at(owner2).at(spender).read(&mut contract)?;
719
720            prop_assert_eq!(after_delete1, U256::ZERO, "owner1 allowance not deleted");
721            prop_assert_eq!(after_delete2, allowance2, "owner2 allowance affected");
722        }
723    }
724
725    // -- RUNTIME SLOT OFFSET TESTS --------------------------------------------
726
727    #[test]
728    fn test_mapping_at_offset() -> eyre::Result<()> {
729        let mut storage = HashMapStorageProvider::new(1);
730        let mut contract = setup_test_contract(&mut storage);
731
732        // Simulate: mapping(bytes32 => Orderbook) books
733        // where Orderbook has a mapping field `bids` at field offset 1
734        let pair_key: B256 = U256::from(0x1234).into();
735        let orderbook_base_slot = mapping_slot(pair_key, TEST_SLOT_1);
736
737        // Use Mapping::*_at_offset() to access the bids mapping within the Orderbook struct
738        let tick: i16 = 100;
739        let bid_value = U256::from(500);
740
741        // Write to orderbook.bids[tick]
742        Mapping::<i16, U256>::at_offset(
743            orderbook_base_slot,
744            1, // bids field is at offset 1 in Orderbook
745            tick,
746        )
747        .write(&mut contract, bid_value)?;
748
749        // Read from orderbook.bids[tick]
750        let read_value =
751            Mapping::<i16, U256>::at_offset(orderbook_base_slot, 1, tick).read(&mut contract)?;
752
753        assert_eq!(read_value, bid_value);
754
755        // Delete orderbook.bids[tick]
756        Mapping::<i16, U256>::at_offset(orderbook_base_slot, 1, tick).delete(&mut contract)?;
757
758        let deleted_value =
759            Mapping::<i16, U256>::at_offset(orderbook_base_slot, 1, tick).read(&mut contract)?;
760
761        assert_eq!(deleted_value, U256::ZERO);
762
763        Ok(())
764    }
765
766    #[test]
767    fn test_nested_mapping_at_offset() -> eyre::Result<()> {
768        let mut storage = HashMapStorageProvider::new(1);
769        let mut contract = setup_test_contract(&mut storage);
770
771        // Simulate a struct with nested mapping at field offset 3
772        let struct_key: B256 = U256::from(0xabcd).into();
773        let struct_base_slot = mapping_slot(struct_key, TEST_SLOT_2);
774
775        let owner = Address::random();
776        let spender = Address::random();
777        let allowance = U256::from(1000);
778
779        // Write to nested_mapping[owner][spender]
780        let field_slot = struct_base_slot + U256::from(3); // nested mapping at field offset 3
781        let nested_mapping = NestedMapping::<Address, Address, U256>::new(field_slot);
782        nested_mapping
783            .at(owner)
784            .at(spender)
785            .write(&mut contract, allowance)?;
786
787        // Read back
788        let read_allowance = nested_mapping.at(owner).at(spender).read(&mut contract)?;
789
790        assert_eq!(read_allowance, allowance);
791
792        // Delete
793        nested_mapping.at(owner).at(spender).delete(&mut contract)?;
794
795        let deleted = nested_mapping.at(owner).at(spender).read(&mut contract)?;
796
797        assert_eq!(deleted, U256::ZERO);
798
799        Ok(())
800    }
801
802    #[test]
803    fn test_multiple_fields_at_different_offsets() -> eyre::Result<()> {
804        let mut storage = HashMapStorageProvider::new(1);
805        let mut contract = setup_test_contract(&mut storage);
806
807        // Simulate Orderbook with multiple mapping fields
808        let pair_key: B256 = U256::from(0x5678).into();
809        let orderbook_base = mapping_slot(pair_key, TEST_SLOT_0);
810
811        // bids at offset 1
812        let tick1: i16 = 50;
813        let bid1 = U256::from(100);
814        Mapping::<i16, U256>::at_offset(orderbook_base, 1, tick1).write(&mut contract, bid1)?;
815
816        // asks at offset 2
817        let tick2: i16 = -25;
818        let ask1 = U256::from(200);
819        Mapping::<i16, U256>::at_offset(orderbook_base, 2, tick2).write(&mut contract, ask1)?;
820
821        // bidBitmap at offset 3
822        let bitmap_key: i16 = 10;
823        let bitmap_value = U256::from(0xff);
824        Mapping::<i16, U256>::at_offset(orderbook_base, 3, bitmap_key)
825            .write(&mut contract, bitmap_value)?;
826
827        // Verify all fields are independent
828        let read_bid =
829            Mapping::<i16, U256>::at_offset(orderbook_base, 1, tick1).read(&mut contract)?;
830        let read_ask =
831            Mapping::<i16, U256>::at_offset(orderbook_base, 2, tick2).read(&mut contract)?;
832        let read_bitmap =
833            Mapping::<i16, U256>::at_offset(orderbook_base, 3, bitmap_key).read(&mut contract)?;
834
835        assert_eq!(read_bid, bid1);
836        assert_eq!(read_ask, ask1);
837        assert_eq!(read_bitmap, bitmap_value);
838
839        Ok(())
840    }
841}