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!
Example: Using enums in storage
Enums use 0-based layout for serialization. For example, FirstVariantOfSomeEnum(100)
will be serialized as [0, 100]
. However, their Starknet storage layout is 1-based for most enums, especially for these with derived Store
trait implementation. Therefore, FirstVariantOfSomeEnum(100)
will be stored on Starknet as [1, 100]
.
Remember that this rule may not hold for enums that with manual Store
trait implementation. The most notable example is Option
, e.g. Option::None
will be stored as [0]
and Option::Some(100)
will be stored as [1, 100]
.
Below is an example of a contract which can store Option<u256>
values:
#[starknet::interface]
pub trait IEnumsStorageContract<TContractState> {
fn read_value(self: @TContractState, key: u256) -> Option<u256>;
}
#[starknet::contract]
pub mod EnumsStorageContract {
use starknet::{storage::{StoragePointerWriteAccess, StoragePathEntry, Map}};
#[storage]
struct Storage {
example_storage: Map<u256, Option<u256>>,
}
#[abi(embed_v0)]
impl EnumsStorageContractImpl of super::IEnumsStorageContract<ContractState> {
fn read_value(self: @ContractState, key: u256) -> Option<u256> {
self.example_storage.entry(key).read()
}
}
}
And a test which uses store
and reads the value:
use direct_storage_access::using_enums::IEnumsStorageContractSafeDispatcherTrait;
use direct_storage_access::using_enums::IEnumsStorageContractSafeDispatcher;
use starknet::ContractAddress;
use snforge_std::{declare, ContractClassTrait, DeclareResultTrait, store, map_entry_address, load};
fn deploy_contract(name: ByteArray) -> ContractAddress {
let contract = declare(name).unwrap().contract_class();
let (contract_address, _) = contract.deploy(@ArrayTrait::new()).unwrap();
contract_address
}
#[test]
fn test_store_and_read() {
let contract_address = deploy_contract("EnumsStorageContract");
let safe_dispatcher = IEnumsStorageContractSafeDispatcher { contract_address };
let mut keys = ArrayTrait::new();
let key: u256 = 1;
key.serialize(ref keys);
let value: Option<u256> = Option::Some((100));
let felt_value: felt252 = value.unwrap().try_into().unwrap();
// Serialize Option enum according to its 1-based storage layout
let serialized_value = array![1, felt_value];
let storage_address = map_entry_address(selector!("example_storage"), keys.span());
store(
target: contract_address,
storage_address: storage_address,
serialized_value: serialized_value.span(),
);
let read_value = safe_dispatcher.read_value(key).expect('Failed to read value');
assert_eq!(read_value, value);
}
snforge test test_store_and_read
Output:
Collected 1 test(s) from direct_storage_access package
Running 1 test(s) from tests/
[PASS] direct_storage_access_tests::using_enums::test_store_and_read (gas: ~233)
Running 0 test(s) from src/
Tests: 1 passed, 0 failed, 0 skipped, 0 ignored, 4 filtered out
📝 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).
Example with storage_address_from_base
This example uses storage_address_from_base
with entry's of the storage variable.
use starknet::storage::StorageAsPointer;
use starknet::storage::StoragePathEntry;
use snforge_std::{declare, ContractClassTrait, DeclareResultTrait, store, load};
use starknet::storage_access::{storage_address_from_base};
use direct_storage_access::felts_only::{
SimpleStorageContract, ISimpleStorageContractDispatcher, ISimpleStorageContractDispatcherTrait
};
#[test]
fn update_mapping() {
let key = 0;
let data = 100;
let (contract_address, _) = declare("SimpleStorageContract")
.unwrap()
.contract_class()
.deploy(@array![])
.unwrap();
let dispatcher = ISimpleStorageContractDispatcher { contract_address };
let mut state = SimpleStorageContract::contract_state_for_testing();
let storage_address = storage_address_from_base(
state.mapping.entry(key).as_ptr().__storage_pointer_address__.into()
);
let storage_value: Span<felt252> = array![data.into()].span();
store(contract_address, storage_address.into(), storage_value);
let read_data = dispatcher.get_value(key.into());
assert_eq!(read_data, data, "Storage update failed")
}