Contract storage

Storage layout

The contract’s storage is a persistent storage space where you can read, write, modify, and persist data. The storage is a map with \(2^{251}\) slots, where each slot is a felt which is initialized to 0.

Storage low level functions

The basic function for writing to storage writes, value to key is:

storage_write_syscall(address_domain, address, value)

storage_read is a basic function that is used for getting the storage address, this function is created by the compiler when defining a storage variable, as explained below. This function returns the address of the storage variable. Below we discuss how this address is determined from the variable’s name and keys.

Both storage_read and storage_write are system calls that can be imported by adding the line:

use starknet::syscalls::storage_read_syscall;
use starknet::syscalls::storage_write_syscall;

Storage variables

The most common way to interact with a contract’s storage is through storage variables.

The #[storage] attribute above the Storage struct declares that the contents of this struct are part of the contract storage. The storage variables stored inside this struct can consist of a single felt, or it can be a mapping from multiple arguments to a tuple of felts or structs.

To use this variable, the var.read(args), var.write(args, value) and var.address(args) functions are automatically created by the #[storage] attribute, for reading the storage value, writing the storage value and getting the storage address, respectively.

The Starknet contract compiler generates the Cairo code that maps the storage variable’s name and argument values to an address — so that it can be part of the generated proof.

The address of a storage variable is computed as follows:

  • If it is a single value, then the address is sn_keccak(variable_name), where variable_name is the ASCII encoding of the variable’s name.

  • If it is a (nested) mapping, then the address of the value at key k_1,...,k_n is h(...h(h(sn_keccak(variable_name),k_1),k_2),...,k_n) where \(h\) is the Pedersen hash and the final value is taken \(\bmod 2^{251}-256\)

  • If it is a mapping to complex values (e.g., tuples or structs), then this complex value lies in a continuous segment starting from the address calculated in the previous point. Note that 256 field elements is the current limitation on the maximal size of a complex storage value.

  • Note that when calling var.address(args) for a storage variable with complex values, the returned value is the address of the first element in the storage.

We can summarize the above as follows:

storage variable address := pedersen(keccak(variable name), keys)

The following example defines storage variables with complex values.

#[storage]
struct Storage {
    name: felt252,
    symbol: felt252,
    decimals: u8,
    total_supply: u256,
    balances: LegacyMap::<ContractAddress, u256>,
    allowances: LegacyMap::<(ContractAddress, ContractAddress), u256>,
}