Direct Storage Access

In some instances, it's not possible for contracts to expose API that we'd like to use in order to initialize the contracts before running some tests. For those cases snforge exposes storage-related cheatcodes, which allow manipulating the storage directly (reading and writing).

In order to obtain the variable address that you'd like to write to, or read from, you need to use either:

  • selector! macro - if the variable is not a mapping
  • map_entry_address function in tandem with selector! - for key-value pair of a map variable
  • starknet::storage_access::storage_address_from_base

Example: Felt-only storage

This example uses only felts for simplicity.

  1. Exact storage fields
use snforge_std::{declare, ContractClassTrait, DeclareResultTrait, store, load, map_entry_address};

#[test]
fn test_store_and_load_plain_felt() {
    let (contract_address, _) = declare("SimpleStorageContract")
        .unwrap()
        .contract_class()
        .deploy(@array![])
        .unwrap();

    // load existing value from storage
    let loaded = load(
        contract_address, // an existing contract which owns the storage
        selector!("plain_felt"), // field marking the start of the memory chunk being read from
        1 // length of the memory chunk (seen as an array of felts) to read
    );

    assert_eq!(loaded, array![0x2137_felt252]);

    // overwrite it with a new value
    store(
        contract_address, // storage owner
        selector!("plain_felt"), // field marking the start of the memory chunk being written to
        array![420].span() // array of felts to write
    );

    // load again and check if it changed
    let loaded = load(contract_address, selector!("plain_felt"), 1);
    assert_eq!(loaded, array![420]);
}
  1. Map entries
use snforge_std::{declare, ContractClassTrait, DeclareResultTrait, store, load, map_entry_address};

#[test]
fn test_store_and_load_map_entries() {
    let (contract_address, _) = declare("SimpleStorageContract")
        .unwrap()
        .contract_class()
        .deploy(@array![])
        .unwrap();

    // load an existing map entry
    let loaded = load(
        contract_address,
        map_entry_address(
            selector!("mapping"), // start of the read memory chunk
            array!['some_key'].span(), // map key
        ),
        1, // length of the read memory chunk
    );

    assert_eq!(loaded, array!['some_value']);

    // write other value in place of the previous one
    store(
        contract_address,
        map_entry_address(
            selector!("mapping"), // storage variable name
             array!['some_key'].span(), // map key
        ),
        array!['some_other_value'].span()
    );

    let loaded = load(
        contract_address,
        map_entry_address(
            selector!("mapping"), // start of the read memory chunk
            array!['some_key'].span(), // map key
        ),
        1, // length of the read memory chunk
    );

    assert_eq!(loaded, array!['some_other_value']);

    // load value written under non-existing key
    let loaded = load(
        contract_address,
        map_entry_address(selector!("mapping"), array!['non_existing_field'].span(),),
        1,
    );

    assert_eq!(loaded, array![0]);
}

Example: Complex structures in storage

This example uses a complex key and value, with default derived serialization methods (via #[derive(starknet::Store)]).

We use a contract along with helper structs:

use core::hash::LegacyHash;

// Required for lookup of complex_mapping values
// This is consistent with `map_entry_address`, which uses pedersen hashing of keys
impl StructuredKeyHash of LegacyHash<MapKey> {
    fn hash(state: felt252, value: MapKey) -> felt252 {
        let state = LegacyHash::<felt252>::hash(state, value.a);
        LegacyHash::<felt252>::hash(state, value.b)
    }
}

#[derive(Copy, Drop, Serde)]
pub struct MapKey {
    pub a: felt252,
    pub b: felt252,
}

// Serialization of keys and values with `Serde` to make usage of `map_entry_address` easier
impl MapKeyIntoSpan of Into<MapKey, Span<felt252>> {
    fn into(self: MapKey) -> Span<felt252> {
        let mut serialized_struct: Array<felt252> = array![];
        self.serialize(ref serialized_struct);
        serialized_struct.span()
    }
}

#[derive(Copy, Drop, Serde, starknet::Store)]
pub struct MapValue {
    pub a: felt252,
    pub b: felt252,
}

impl MapValueIntoSpan of Into<MapValue, Span<felt252>> {
    fn into(self: MapValue) -> Span<felt252> {
        let mut serialized_struct: Array<felt252> = array![];
        self.serialize(ref serialized_struct);
        serialized_struct.span()
    }
}

#[starknet::interface]
pub trait IComplexStorageContract<TContractState> {}

#[starknet::contract]
mod ComplexStorageContract {
    use starknet::storage::Map;
    use super::{MapKey, MapValue};

    #[storage]
    struct Storage {
        complex_mapping: Map<MapKey, MapValue>,
    }
}

And perform a test checking load and store behavior in context of those structs:

use snforge_std::{declare, ContractClassTrait, DeclareResultTrait, store, load, map_entry_address};

use direct_storage_access::complex_structures::{MapKey, MapValue};

#[test]
fn store_in_complex_mapping() {
    let (contract_address, _) = declare("ComplexStorageContract")
        .unwrap()
        .contract_class()
        .deploy(@array![])
        .unwrap();

    let k = MapKey { a: 111, b: 222 };
    let v = MapValue { a: 123, b: 456 };

    store(
        contract_address,
        map_entry_address( // uses Pedersen hashing under the hood for address calculation
            selector!("mapping"), // storage variable name
             k.into() // map key
        ),
        v.into()
    );

    // complex_mapping = {
    //     hash(k): 123,
    //     hash(k) + 1: 456
    //     ...
    // }

    let loaded = load(contract_address, map_entry_address(selector!("mapping"), k.into()), 2,);

    assert_eq!(loaded, array![123, 456]);
}

⚠️ Warning

Complex data can often times be packed in a custom manner (see this pattern) to optimize costs. If that's the case for your contract, make sure to handle deserialization properly - standard methods might not work. Use those cheatcode as a last-resort, for cases that cannot be handled via contract's API!

📝 Note

The load cheatcode will return zeros for memory you haven't written into yet (it is a default storage value for Starknet contracts' storage).