Storing Key-Value Pairs with Mappings
Storage mappings in Cairo provide a way to associate keys with values and persist them in the contract's storage. Unlike traditional hash tables, storage mappings do not store the key data itself; instead, they use the hash of the key to compute an address that corresponds to the storage slot where the corresponding value is stored. Therefore, it is not possible to iterate over the keys of a storage mapping.
Mappings do not have a concept of length or whether a key-value pair is set. All
values are by default set to 0. As such, the only way to remove an entry from a
mapping is to set its value to the default value for the type, which would be
0 for the u64 type.
The Map type, provided by the Cairo core library, inside the
core::starknet::storage module, is used to declare mappings in contracts.
To declare a mapping, use the Map type enclosed in angle brackets <>,
specifying the key and value types. In Listing 15-2, we
create a simple contract that stores values mapped to the caller's address.
The
Felt252Dicttype is a memory type that cannot be stored in contract storage. For persistent storage of key-value pairs, use theMaptype, which is a [phantom type][phantom types] designed specifically for contract storage. However,Maphas limitations: it can't be instantiated as a regular variable, used as a function parameter, or included as a member in regular structs.Mapcan only be used as a storage variable within a contract's storage struct. To work with the contents of aMapin memory or perform complex operations, you'll need to copy its elements to and from aFelt252Dictor other suitable data structure.
Declaring and Using Storage Mappings
use starknet::ContractAddress;
#[starknet::interface]
trait IUserValues<TState> {
fn set(ref self: TState, amount: u64);
fn get(self: @TState, address: ContractAddress) -> u64;
}
#[starknet::contract]
mod UserValues {
use starknet::storage::{
Map, StoragePathEntry, StoragePointerReadAccess, StoragePointerWriteAccess,
};
use starknet::{ContractAddress, get_caller_address};
#[storage]
struct Storage {
user_values: Map<ContractAddress, u64>,
}
#[abi(embed_v0)]
impl UserValuesImpl of super::IUserValues<ContractState> {
fn set(ref self: ContractState, amount: u64) {
let caller = get_caller_address();
self.user_values.entry(caller).write(amount);
}
fn get(self: @ContractState, address: ContractAddress) -> u64 {
self.user_values.entry(address).read()
}
}
}
#[cfg(test)]
mod tests;
15-2: Declaring a storage mapping in the Storage struct
To read the value corresponding to a key in a mapping, you first need to
retrieve the storage pointer associated with that key. This is done by calling
the entry method on the storage mapping variable, passing in the key as a
parameter. Once you have the entry path, you can call the read function on it
to retrieve the stored value.
use starknet::ContractAddress;
#[starknet::interface]
trait IUserValues<TState> {
fn set(ref self: TState, amount: u64);
fn get(self: @TState, address: ContractAddress) -> u64;
}
#[starknet::contract]
mod UserValues {
use starknet::storage::{
Map, StoragePathEntry, StoragePointerReadAccess, StoragePointerWriteAccess,
};
use starknet::{ContractAddress, get_caller_address};
#[storage]
struct Storage {
user_values: Map<ContractAddress, u64>,
}
#[abi(embed_v0)]
impl UserValuesImpl of super::IUserValues<ContractState> {
fn set(ref self: ContractState, amount: u64) {
let caller = get_caller_address();
self.user_values.entry(caller).write(amount);
}
fn get(self: @ContractState, address: ContractAddress) -> u64 {
self.user_values.entry(address).read()
}
}
}
#[cfg(test)]
mod tests;
Similarly, to write a value in a storage mapping, you need to retrieve the
storage pointer corresponding to the key. Once you have this storage pointer,
you can call the write function on it with the value to write.
use starknet::ContractAddress;
#[starknet::interface]
trait IUserValues<TState> {
fn set(ref self: TState, amount: u64);
fn get(self: @TState, address: ContractAddress) -> u64;
}
#[starknet::contract]
mod UserValues {
use starknet::storage::{
Map, StoragePathEntry, StoragePointerReadAccess, StoragePointerWriteAccess,
};
use starknet::{ContractAddress, get_caller_address};
#[storage]
struct Storage {
user_values: Map<ContractAddress, u64>,
}
#[abi(embed_v0)]
impl UserValuesImpl of super::IUserValues<ContractState> {
fn set(ref self: ContractState, amount: u64) {
let caller = get_caller_address();
self.user_values.entry(caller).write(amount);
}
fn get(self: @ContractState, address: ContractAddress) -> u64 {
self.user_values.entry(address).read()
}
}
}
#[cfg(test)]
mod tests;
Nested Mappings
You can also create more complex mappings with multiple keys. To illustrate this, we'll implement a contract representing warehouses assigned to users, where each user can store multiple items with their respective quantities.
The user_warehouse mapping is a storage mapping that maps ContractAddress to
another mapping that maps u64 (item ID) to u64 (quantity). This can be
implemented by declaring a Map<ContractAddress, Map<u64, u64>> in the storage
struct. Each ContractAddress key in the user_warehouse mapping corresponds
to a user's warehouse, and each user's warehouse contains a mapping of item IDs
to their respective quantities.
use starknet::ContractAddress;
#[starknet::interface]
trait IWarehouseContract<TState> {
fn set_quantity(ref self: TState, item_id: u64, quantity: u64);
fn get_item_quantity(self: @TState, address: ContractAddress, item_id: u64) -> u64;
}
#[starknet::contract]
mod WarehouseContract {
use starknet::storage::{
Map, StoragePathEntry, StoragePointerReadAccess, StoragePointerWriteAccess,
};
use starknet::{ContractAddress, get_caller_address};
#[storage]
struct Storage {
user_warehouse: Map<ContractAddress, Map<u64, u64>>,
}
#[abi(embed_v0)]
impl WarehouseContractImpl of super::IWarehouseContract<ContractState> {
fn set_quantity(ref self: ContractState, item_id: u64, quantity: u64) {
let caller = get_caller_address();
self.user_warehouse.entry(caller).entry(item_id).write(quantity);
}
fn get_item_quantity(self: @ContractState, address: ContractAddress, item_id: u64) -> u64 {
self.user_warehouse.entry(address).entry(item_id).read()
}
}
}
#[cfg(test)]
mod tests {
use snforge_std::{
ContractClassTrait, DeclareResultTrait, declare, start_cheat_caller_address,
stop_cheat_caller_address,
};
use starknet::ContractAddress;
use super::{IWarehouseContractDispatcher, IWarehouseContractDispatcherTrait};
fn deploy_contract() -> IWarehouseContractDispatcher {
let contract = declare("WarehouseContract").unwrap().contract_class();
let (contract_address, _) = contract.deploy(@array![]).unwrap();
IWarehouseContractDispatcher { contract_address }
}
#[test]
fn test_default_quantity_is_zero() {
let dispatcher = deploy_contract();
let user: ContractAddress = 0x111.try_into().unwrap();
assert_eq!(dispatcher.get_item_quantity(user, 1), 0);
}
#[test]
fn test_set_and_get_quantity_for_caller() {
let dispatcher = deploy_contract();
let user: ContractAddress = 0x222.try_into().unwrap();
start_cheat_caller_address(dispatcher.contract_address, user);
dispatcher.set_quantity(10, 42);
stop_cheat_caller_address(dispatcher.contract_address);
assert_eq!(dispatcher.get_item_quantity(user, 10), 42);
// Different item id should remain zero
assert_eq!(dispatcher.get_item_quantity(user, 11), 0);
}
#[test]
fn test_quantities_are_per_user() {
let dispatcher = deploy_contract();
let user1: ContractAddress = 0xabc.try_into().unwrap();
let user2: ContractAddress = 0xdef.try_into().unwrap();
start_cheat_caller_address(dispatcher.contract_address, user1);
dispatcher.set_quantity(1, 10);
stop_cheat_caller_address(dispatcher.contract_address);
start_cheat_caller_address(dispatcher.contract_address, user2);
dispatcher.set_quantity(1, 7);
stop_cheat_caller_address(dispatcher.contract_address);
assert_eq!(dispatcher.get_item_quantity(user1, 1), 10);
assert_eq!(dispatcher.get_item_quantity(user2, 1), 7);
// Ensure other item for user1 remains zero
assert_eq!(dispatcher.get_item_quantity(user1, 2), 0);
}
#[test]
fn test_update_quantity_overwrites_value() {
let dispatcher = deploy_contract();
let user: ContractAddress = 0x303.try_into().unwrap();
start_cheat_caller_address(dispatcher.contract_address, user);
dispatcher.set_quantity(5, 1);
dispatcher.set_quantity(5, 99);
stop_cheat_caller_address(dispatcher.contract_address);
assert_eq!(dispatcher.get_item_quantity(user, 5), 99);
}
}
In this case, the same principle applies for accessing the stored values. You
need to traverse the keys step by step, using the entry method to get the
storage path to the next key in the sequence, and finally calling read or
write on the innermost mapping.
use starknet::ContractAddress;
#[starknet::interface]
trait IWarehouseContract<TState> {
fn set_quantity(ref self: TState, item_id: u64, quantity: u64);
fn get_item_quantity(self: @TState, address: ContractAddress, item_id: u64) -> u64;
}
#[starknet::contract]
mod WarehouseContract {
use starknet::storage::{
Map, StoragePathEntry, StoragePointerReadAccess, StoragePointerWriteAccess,
};
use starknet::{ContractAddress, get_caller_address};
#[storage]
struct Storage {
user_warehouse: Map<ContractAddress, Map<u64, u64>>,
}
#[abi(embed_v0)]
impl WarehouseContractImpl of super::IWarehouseContract<ContractState> {
fn set_quantity(ref self: ContractState, item_id: u64, quantity: u64) {
let caller = get_caller_address();
self.user_warehouse.entry(caller).entry(item_id).write(quantity);
}
fn get_item_quantity(self: @ContractState, address: ContractAddress, item_id: u64) -> u64 {
self.user_warehouse.entry(address).entry(item_id).read()
}
}
}
#[cfg(test)]
mod tests {
use snforge_std::{
ContractClassTrait, DeclareResultTrait, declare, start_cheat_caller_address,
stop_cheat_caller_address,
};
use starknet::ContractAddress;
use super::{IWarehouseContractDispatcher, IWarehouseContractDispatcherTrait};
fn deploy_contract() -> IWarehouseContractDispatcher {
let contract = declare("WarehouseContract").unwrap().contract_class();
let (contract_address, _) = contract.deploy(@array![]).unwrap();
IWarehouseContractDispatcher { contract_address }
}
#[test]
fn test_default_quantity_is_zero() {
let dispatcher = deploy_contract();
let user: ContractAddress = 0x111.try_into().unwrap();
assert_eq!(dispatcher.get_item_quantity(user, 1), 0);
}
#[test]
fn test_set_and_get_quantity_for_caller() {
let dispatcher = deploy_contract();
let user: ContractAddress = 0x222.try_into().unwrap();
start_cheat_caller_address(dispatcher.contract_address, user);
dispatcher.set_quantity(10, 42);
stop_cheat_caller_address(dispatcher.contract_address);
assert_eq!(dispatcher.get_item_quantity(user, 10), 42);
// Different item id should remain zero
assert_eq!(dispatcher.get_item_quantity(user, 11), 0);
}
#[test]
fn test_quantities_are_per_user() {
let dispatcher = deploy_contract();
let user1: ContractAddress = 0xabc.try_into().unwrap();
let user2: ContractAddress = 0xdef.try_into().unwrap();
start_cheat_caller_address(dispatcher.contract_address, user1);
dispatcher.set_quantity(1, 10);
stop_cheat_caller_address(dispatcher.contract_address);
start_cheat_caller_address(dispatcher.contract_address, user2);
dispatcher.set_quantity(1, 7);
stop_cheat_caller_address(dispatcher.contract_address);
assert_eq!(dispatcher.get_item_quantity(user1, 1), 10);
assert_eq!(dispatcher.get_item_quantity(user2, 1), 7);
// Ensure other item for user1 remains zero
assert_eq!(dispatcher.get_item_quantity(user1, 2), 0);
}
#[test]
fn test_update_quantity_overwrites_value() {
let dispatcher = deploy_contract();
let user: ContractAddress = 0x303.try_into().unwrap();
start_cheat_caller_address(dispatcher.contract_address, user);
dispatcher.set_quantity(5, 1);
dispatcher.set_quantity(5, 99);
stop_cheat_caller_address(dispatcher.contract_address);
assert_eq!(dispatcher.get_item_quantity(user, 5), 99);
}
}
Storage Address Computation for Mappings
The address in storage of a variable stored in a mapping is computed according to the following rules:
- For a single key
k, the address of the value at keykish(sn_keccak(variable_name), k), wherehis the Pedersen hash and the final value is taken modulo \( {2^{251}} - 256\). - For multiple keys, the address is computed as
h(...h(h(sn_keccak(variable_name), k_1), k_2), ..., k_n), withk_1, ..., k_nbeing all keys that constitute the mapping.
If the key of a mapping is a struct, each element of the struct constitutes a
key. Moreover, the struct should implement the Hash trait, which can be
derived with the #[derive(Hash)] attribute.
Summary
- Storage mappings allow you to map keys to values in contract storage.
- Use the
Maptype to declare mappings. - Access mappings using the
entrymethod andread/writefunctions. - Mappings can contain other mappings, creating nested storage mappings.
- The address of a mapping variable is computed using the
sn_keccakand the Pedersen hash functions.