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 mappingmap_entry_address
function in tandem withselector!
- for key-value pair of a map variablestarknet::storage_access::storage_address_from_base
Example: Felt-only storage
This example uses only felts for simplicity.
- 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]);
}
- 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).